diff --git a/AGENTS.md b/AGENTS.md index 5b22c73..7d112f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,12 +41,14 @@ bun run compile # Compile server to standalone binary bun run fix # Lint + format (Biome auto-fix) bun run typecheck # TypeScript check across monorepo -# Database (in apps/server) -bun run db:generate # Generate migrations from schema -bun run db:migrate # Run migrations -bun run db:push # Push schema (dev only) +# Database (in apps/server) — ALWAYS use migration workflow, never db:push +bun run db:generate # Generate migrations from schema changes +bun run db:migrate # Run pending migrations bun run db:studio # Open Drizzle Studio +# Server CLI (in apps/server) +bun run cli auth reset-password # Reset owner password + # Testing (not yet configured) bun test path/to/test.ts # Run single test file ``` @@ -93,6 +95,7 @@ Biome auto-organizes. Order: 1) External packages → 2) Internal `@/*` aliases - **Validation**: `drizzle-orm/zod` (built-in, NOT separate `drizzle-zod` package) - **Relations**: Defined via `defineRelations()` — RQBv2 object syntax - **Query style**: `db.query.tableName.findMany({ orderBy: { createdAt: 'desc' }, where: { id: 1 } })` +- **Migration workflow**: Always `db:generate` → `db:migrate`. **Never** use `db:push`. ## Environment Variables diff --git a/apps/server/AGENTS.md b/apps/server/AGENTS.md index 3a9e9e5..b2b20f0 100644 --- a/apps/server/AGENTS.md +++ b/apps/server/AGENTS.md @@ -11,7 +11,8 @@ TanStack Start fullstack web app with ORPC (contract-first RPC) and shadcn/ui. - **Database**: PostgreSQL + Drizzle ORM v1 beta (`drizzle-orm/postgres-js`, RQBv2) - **State**: TanStack Query v5 (with MutationCache auto-invalidation) - **RPC**: ORPC (contract-first, type-safe) -- **Auth**: Better Auth (email+password, self-hosted) +- **Auth**: Better Auth (email+password, single-owner, self-hosted) +- **CLI**: citty (server-side admin commands) - **DnD**: @dnd-kit/react + @dnd-kit/helpers (`move()` for sortable) - **Virtualization**: @tanstack/react-virtual (`useVirtualizer`) - **Build**: Vite + Nitro @@ -24,10 +25,11 @@ TanStack Start fullstack web app with ORPC (contract-first RPC) and shadcn/ui. / → Dashboard homepage (bookmark display, daily use) /admin → Admin panel overview (module cards) /admin/bookmarks → Bookmark management (CRUD, DnD, Dialog forms) -/login → Login page -/signup → Signup page +/setup → One-time owner setup (first visit only, redirects to /login after) +/login → Login page (redirects to /setup if no owner exists) ``` +- **Single-owner model**: Kairos is a self-hosted Life OS. Only ONE user (the owner) exists. There is NO registration page — `/setup` is a one-time wizard shown on first visit. - **Display pages** (`/`): Clean, no management UI. What users see daily. - **Admin pages** (`/admin/*`): Full CRUD, management, configuration. Sidebar navigation. - All authenticated routes under `_protected` layout (auth guard → redirect to `/login`). @@ -63,6 +65,9 @@ src/ ├── modules/ │ ├── registry.ts # ModuleMetadata interface + modules[] │ └── bookmarks/ # Bookmarks module (schema, contract, router, components/) +├── cli/ +│ ├── index.ts # citty CLI entrypoint (bun run cli ...) +│ └── commands/auth.ts # auth reset-password command ├── routes/ # TanStack Router file routes │ ├── __root.tsx # Root layout (HTML shell, Toaster) │ ├── _protected.tsx # Auth guard layout @@ -70,7 +75,7 @@ src/ │ │ ├── index.tsx # Dashboard homepage │ │ ├── admin.tsx # Admin layout (SidebarProvider) │ │ └── admin/bookmarks.tsx -│ ├── login.tsx, signup.tsx +│ ├── login.tsx, setup.tsx │ └── api/ # $.ts (OpenAPI), auth.$.ts, health.ts, rpc.$.ts ├── server/ │ ├── api/ @@ -81,7 +86,7 @@ src/ │ │ ├── context.ts # BaseContext type │ │ ├── server.ts # `os = implement(contract).$context()` │ │ └── types.ts # RouterClient type export -│ ├── auth/ # Better Auth (schema, instance, client, getSession) +│ ├── auth/ # Better Auth (schema, instance, client, getSession, checkInitialized) │ └── db/ # Drizzle (fields, relations, singleton instance) ├── env.ts # @t3-oss/env-core validation ├── router.tsx # TanStack Router + QueryClient + MutationCache @@ -247,6 +252,8 @@ toast.error('操作失败') - **Table naming**: No `Table` suffix — `user`, `category`, `bookmark` - **DB instance**: Module-level singleton `export const db = drizzle(...)` (NOT factory pattern) - **Shared fields**: Use `...generatedFields` spread for id/createdAt/updatedAt +- **Migration workflow**: Always `db:generate` → `db:migrate`. **Never** use `db:push`. +- **Path alias exception**: Files in the Drizzle schema chain (`db/schema/index.ts`, module `schema.ts`) MUST use relative imports — `drizzle-kit` does not resolve `@/*` aliases. ```typescript // Schema — use generatedFields spread @@ -264,14 +271,20 @@ export const relations = defineRelations(schema, (r) => ({ })) ``` -## Auth (Better Auth) +## Auth (Better Auth — Single-Owner Model) + +Kairos is a self-hosted single-user app. There is NO public registration. The first visit triggers a one-time setup wizard (`/setup`), and all subsequent signup attempts are blocked at the database level. - **Instance**: `src/server/auth/index.ts` — `betterAuth()` with `drizzleAdapter(db, { provider: 'pg', schema })` +- **Signup blocking**: `databaseHooks.user.create.before` checks if a user already exists → throws `APIError('FORBIDDEN')` if so - **Client**: `src/server/auth/client.ts` — `createAuthClient()` for React -- **Server function**: `src/server/auth/functions.ts` — `getSession()` via `createServerFn` +- **Server functions**: `src/server/auth/functions.ts`: + - `getSession()` — get current session via `createServerFn` + - `checkInitialized()` — check if owner account exists (used by `/setup` and `/login` routes) - **Auth tables**: Use `text` IDs (Better Auth manages its own IDs), NOT project's UUID v7 - **Route guard**: `beforeLoad` in `_protected.tsx` calls `getSession()` → redirect to `/login` - **ORPC middleware**: `authMiddleware` calls `auth.api.getSession({ headers })` → injects `context.user` +- **Password reset**: Via server CLI only (`bun run cli auth reset-password`) — no web-based password recovery ## Critical Rules @@ -294,3 +307,6 @@ export const relations = defineRelations(schema, (r) => ({ - Pass `schema` to `drizzle()` constructor (only `relations` needed in RQBv2) - Add `Table` suffix to Drizzle table exports - Use `useRef` for scroll elements inside Dialog/conditional rendering +- Use `db:push` — always use `db:generate` → `db:migrate` +- Use `@/*` aliases in Drizzle schema files (drizzle-kit can't resolve them) +- Add registration/signup functionality (single-owner model, enforced by `databaseHooks`)