docs: 同步 AGENTS.md — 单 owner 模型 + 强制 migration workflow + CLI 文档
This commit is contained in:
+23
-7
@@ -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<BaseContext>()`
|
||||
│ │ └── 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`)
|
||||
|
||||
Reference in New Issue
Block a user