Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/web/.gitignore 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
9 changes: 9 additions & 0 deletions apps/web/AGENTS 2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

## This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
36 changes: 36 additions & 0 deletions apps/web/README 2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
22 changes: 22 additions & 0 deletions apps/web/eslint.config 2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
// OpenNext and Wrangler output:
".open-next/**",
".wrangler/**",
"cloudflare-env.d.ts",
]),
]);

export default eslintConfig;
18 changes: 18 additions & 0 deletions apps/web/next.config 2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
cacheComponents: true,
partialPrefetching: true,
reactCompiler: true,
turbopack: {
rules: {
"*.css": {
loaders: ["@tailwindcss/turbopack"],
as: "*.css",
},
},
},
};

export default nextConfig;
31 changes: 31 additions & 0 deletions apps/web/package 2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@gitwork/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"typecheck": "next typegen && tsc --noEmit",
"cf:build": "opennextjs-cloudflare build",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview"
},
"dependencies": {
"next": "16.4.2",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@tailwindcss/turbopack": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.4.2",
"tailwindcss": "^4",
"typescript": "^5"
},
"license": "AGPL-3.0-only"
}
1 change: 1 addition & 0 deletions apps/web/public/file 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/globe 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/next 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/vercel 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/window 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/src/app/favicon 2.ico
Binary file not shown.
26 changes: 26 additions & 0 deletions apps/web/src/app/globals 2.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import "tailwindcss";

:root {
--background: #ffffff;
--foreground: #171717;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
30 changes: 30 additions & 0 deletions apps/web/src/app/layout 2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { APP_NAME } from "@gitwork/core";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: APP_NAME,
description: "Project management on top of GitHub.",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}
10 changes: 10 additions & 0 deletions apps/web/src/app/page 2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { APP_NAME } from "@gitwork/core";

export default function Home() {
return (
<main className="flex flex-1 flex-col items-center justify-center gap-2 p-8">
<h1 className="text-3xl font-semibold">{APP_NAME}</h1>
<p className="text-zinc-600 dark:text-zinc-400">GitHub is the source of truth.</p>
</main>
);
}
34 changes: 34 additions & 0 deletions apps/web/tsconfig 2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
25 changes: 25 additions & 0 deletions apps/web/worker 2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Custom Worker entry (decision 004): OpenNext's fetch handler plus the reconcile Cron Trigger.
// OpenNext builds .open-next/worker.js; wrangler bundles this file around it.
import { openD1 } from "@gitwork/db/d1";
import { runScheduledReconcile } from "@gitwork/server/sync";
// Generated by `opennextjs-cloudflare build`; absent before the first build, so @ts-ignore.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { default as handler } from "./.open-next/worker.js";

export default {
fetch: handler.fetch,

async scheduled(_controller, env, ctx) {
ctx.waitUntil(
runScheduledReconcile(openD1(env.DB), env as unknown as Record<string, string | undefined>, crypto.randomUUID()).then((run) =>
console.log(`[reconcile] ${JSON.stringify(run)}`),
),
);
},
} satisfies ExportedHandler<CloudflareEnv>;

// Generated by `opennextjs-cloudflare build`; absent before the first build, so @ts-ignore.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export { DOQueueHandler, DOShardedTagCache, BucketCachePurge } from "./.open-next/worker.js";
1 change: 1 addition & 0 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
| 019 | [Instance admins and email from the environment; wizard creates the GitHub App](decisions/019-instance-setup.md) |
| 020 | [Linking a workspace is checked with the user's own GitHub token](decisions/020-workspace-link-boundary.md) |
| 021 | [Task writes: user token first, App on refusal, synchronous](decisions/021-task-writes.md) |
| 022 | [Cloudflare hosting requires the Workers Paid plan](decisions/022-cloudflare-paid-plan.md) |

## Features

Expand Down
12 changes: 4 additions & 8 deletions docs/STATE.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# State

Last updated: 2026-10-15
Last updated: 2026-10-16

## Resume here

- **Phase 3 (tasks on web) is in review** on branch `feature/tasks` (PR pending owner merge).
PRs #1–#3 (Phases 0–2) are merged.
- PRs #1–#4 (Phases 0–3) are merged. Next: Phase 4 — Docs on web.
- **Live GitHub check: still not done.** It needs a person (or a connected browser) to click
"Create GitHub App" on github.com, and a public webhook URL. See Chores.
- **Before any work:** `git fetch origin && git status -sb`.
Expand All @@ -24,7 +23,7 @@ None. Next step: Phase 4 — Docs on web.
| Phase 0 — Scaffold | 2026-10-15 | [features/scaffold.md](features/scaffold.md) |
| Phase 1 — Identity | 2026-10-15 | [features/identity.md](features/identity.md) |
| Phase 2 — GitHub App (PR #3) | 2026-10-15 | [features/github-app.md](features/github-app.md) |
| Phase 3 — Tasks on web (in review) | 2026-10-15 | [features/tasks.md](features/tasks.md) |
| Phase 3 — Tasks on web (PR #4) | 2026-10-16 | [features/tasks.md](features/tasks.md) |

## Backlog

Expand All @@ -42,10 +41,7 @@ Chores:
(`GET /app/hook/deliveries/{id}`) and replay them to the local webhook route, re-signed with the
stored secret (the API returns parsed JSON, not the signed bytes). Diff real payloads against
`test/fake-github.ts`; exercise linking and a task write with a real user token.
- Workers bundle is 3.40 MiB gzipped, over the Free plan's 3 MiB (main was already 3.23 MiB).
Decide: require the Paid plan for Cloudflare, or cut the bundle.
- Owner question (decision 020): should a workspace unlink when the Admin who linked it loses
GitHub access?
- `pnpm smoke`: fail when the gzipped Worker bundle passes 10 MiB (decision 022).
- Before a hosted launch: `/github/installed` should show org names only to users whose GitHub
token can see the installation (now it shows none).
- Upgrade Next 16.3.8 → 16.4.x with OpenNext ≥1.20.12 once it passes the pnpm age policy.
Expand Down
2 changes: 2 additions & 0 deletions docs/decisions/009-tasks-projects-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ Projects v2 as the task source.
## Consequences
- Requires the org Projects permission, so org-owned boards only (006).
- Projects v2 webhooks are in public preview; reconcile covers gaps (010).
- Field mapping has "Auto-detect" (by GitHub's usual field names) but no "none": a mapped field can
be overridden, not switched off. (Owner confirmed 2026-10-16.)
34 changes: 34 additions & 0 deletions docs/decisions/017-identity 2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 017 — Identity model, GitHub gate, sign-up policy

Date: 2026-10-15 · Status: Accepted · Narrows 007 · Supersedes part of 005

## Context
Phase 1 builds sign-in, accounts, workspaces, roles, and invites. Three questions came up: what
Better Auth should own, which Owner/Admin capabilities need a linked GitHub account, and who may
create a new Account.

## Decision
- **Better Auth owns identity only:** users, sessions, verification codes, linked GitHub identities.
Gitwork owns accounts, account owners, workspaces, memberships, and invites in its own tables.
Better Auth's `organization` plugin is not used. Better Auth's "account" model is stored as
`auth_identities` so it never collides with Gitwork's Account.
- **GitHub gate (owner decision):** a linked GitHub account is needed only for GitHub tasks —
installing the App and setting a workspace's repos, board, and fields. Owners and Admins can
invite, manage members, and change other settings without GitHub. The gated set is one constant,
`GITHUB_CAPABILITIES`, in `packages/core`.
- **Sign-up policy (owner decision):** an instance setting, `open` or `invite-only`. Default `open`
until the Phase 2 setup wizard exists; then self-host defaults to `invite-only` and the hosted
version stays `open`. A person with a valid pending invite can always sign up. Joining a
workspace always needs an invite.

## Alternatives considered
- **Better Auth `organization` plugin:** has roles and invites, but no Account level above
workspaces and no GitHub gate.
- **Gate all Owner/Admin power on GitHub:** a new Owner could not invite anyone before linking.
- **Always open / always invite-only sign-up:** the first suits hosting but not private installs;
the second leaves the hosted version no way to onboard customers.

## Consequences
- Every permission check calls `packages/core`; the matrix tests cover the gate.
- The sign-up check is a hook before user creation, not Better Auth's global `disableSignUp`, which
would lock out invitees.
Loading
Loading