create-kuckit-app 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kuckit-app",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Create a new Kuckit application",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,6 +71,209 @@ adapters → Implements ports, depends on domain
71
71
  api → Wires everything together
72
72
  ```
73
73
 
74
+ ## SDK Module System
75
+
76
+ > **SDK Documentation**: For comprehensive module patterns, see [@kuckit/sdk](https://github.com/draphonix/kuckit)
77
+
78
+ ### Module Lifecycle
79
+
80
+ Modules are loaded in a specific sequence:
81
+
82
+ ```
83
+ 1. createKuckitContainer()
84
+ └─► Core services registered (logger, db, cache, eventBus)
85
+
86
+ 2. loadKuckitModules({ modules: [...] })
87
+ ├─► Phase 1: register() - DI bindings for all modules
88
+ ├─► Phase 2: registerApi() - API registrations collected
89
+ ├─► Phase 3: onApiRegistrations callback (wire routers HERE)
90
+ ├─► Phase 4: onBootstrap() - Startup logic
91
+ └─► Phase 5: onComplete callback
92
+
93
+ 3. Application runs...
94
+
95
+ 4. disposeContainer()
96
+ └─► For each module: onShutdown()
97
+ ```
98
+
99
+ ### Core Services (CoreCradle)
100
+
101
+ After container creation, these services are available via DI:
102
+
103
+ | Token | Type | Lifetime | Description |
104
+ | ------------------ | ------------------ | --------- | --------------------------- |
105
+ | `config` | `CoreConfig` | Singleton | Application configuration |
106
+ | `db` | Drizzle | Singleton | Database query builder |
107
+ | `dbPool` | `Pool` | Singleton | PostgreSQL connection pool |
108
+ | `logger` | `Logger` | Singleton | Structured logging |
109
+ | `eventBus` | `EventBus` | Singleton | Pub/sub event system |
110
+ | `clock` | `Clock` | Singleton | Time abstraction (testable) |
111
+ | `cacheStore` | `CacheStore` | Singleton | Key-value cache |
112
+ | `rateLimiterStore` | `RateLimiterStore` | Singleton | Rate limiting |
113
+ | `auth` | Better-Auth | Singleton | Authentication utilities |
114
+ | `requestId` | `string` | Scoped | Per-request unique ID |
115
+ | `requestLogger` | `Logger` | Scoped | Logger with request context |
116
+
117
+ ### Server Module Definition
118
+
119
+ ```typescript
120
+ import { defineKuckitModule, asClass, asFunction } from '@kuckit/sdk'
121
+
122
+ export const kuckitModule = defineKuckitModule({
123
+ id: 'myapp.billing',
124
+ displayName: 'Billing',
125
+ version: '1.0.0',
126
+ capabilities: ['nav.item', 'api.public'],
127
+
128
+ register(ctx) {
129
+ // Phase 1: Register DI bindings
130
+ ctx.container.register({
131
+ invoiceRepository: asClass(InvoiceRepository).scoped(),
132
+ createInvoice: asFunction(makeCreateInvoiceUseCase).scoped(),
133
+ })
134
+ },
135
+
136
+ registerApi(ctx) {
137
+ // Phase 2: Register API routes
138
+ ctx.addApiRegistration({
139
+ type: 'rpc-router',
140
+ name: 'invoices',
141
+ router: invoicesRouter,
142
+ })
143
+ },
144
+
145
+ onBootstrap(ctx) {
146
+ // Phase 4: Startup logic (cache warming, logging, etc.)
147
+ ctx.container.resolve('logger').info('Billing module started')
148
+ },
149
+
150
+ onShutdown(ctx) {
151
+ // Cleanup on shutdown
152
+ ctx.container.resolve('logger').info('Billing module stopped')
153
+ },
154
+ })
155
+ ```
156
+
157
+ ### Client Module Definition
158
+
159
+ ```typescript
160
+ import { defineKuckitClientModule } from '@kuckit/sdk-react'
161
+
162
+ export const kuckitClientModule = defineKuckitClientModule({
163
+ id: 'myapp.billing',
164
+ displayName: 'Billing',
165
+ capabilities: ['nav.item', 'dashboard.widget'],
166
+
167
+ routes: [
168
+ {
169
+ id: 'billing-invoices',
170
+ path: '/billing/invoices',
171
+ component: InvoicesPage,
172
+ },
173
+ ],
174
+
175
+ navItems: [
176
+ {
177
+ id: 'billing-nav',
178
+ label: 'Billing',
179
+ href: '/billing/invoices',
180
+ icon: CreditCard,
181
+ order: 50,
182
+ },
183
+ ],
184
+
185
+ slots: {
186
+ 'dashboard.widgets': {
187
+ component: BillingWidget,
188
+ order: 10,
189
+ },
190
+ },
191
+ })
192
+ ```
193
+
194
+ ### Module Capabilities
195
+
196
+ Modules can declare capabilities for discovery:
197
+
198
+ - `nav.item` - Provides navigation items
199
+ - `settings.page` - Has settings page
200
+ - `dashboard.widget` - Provides dashboard widgets
201
+ - `api.webhook` - Exposes webhooks
202
+ - `api.public` - Has public API endpoints
203
+ - `slot.provider` - Provides slot components
204
+
205
+ ### useRpc with TanStack Query
206
+
207
+ Module components use the `useRpc` hook to access the API:
208
+
209
+ ```typescript
210
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
211
+ import { useRpc } from '@kuckit/sdk-react'
212
+
213
+ interface ItemsRpc {
214
+ items: {
215
+ list: (input: Record<string, never>) => Promise<Item[]>
216
+ create: (input: { name: string }) => Promise<Item>
217
+ }
218
+ }
219
+
220
+ function ItemsPage() {
221
+ const rpc = useRpc<ItemsRpc>()
222
+ const queryClient = useQueryClient()
223
+
224
+ const { data: items = [], isLoading } = useQuery({
225
+ queryKey: ['items'],
226
+ queryFn: () => rpc.items.list({}),
227
+ })
228
+
229
+ const createMutation = useMutation({
230
+ mutationFn: (data: { name: string }) => rpc.items.create(data),
231
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
232
+ })
233
+
234
+ // ... component render
235
+ }
236
+ ```
237
+
238
+ ### oRPC Router Wiring (Important)
239
+
240
+ oRPC's `RPCHandler` captures the router object at construction time. Module routers must be wired **before** the handler is created:
241
+
242
+ 1. Modules register routers via `registerApi()` hook
243
+ 2. Server wires routers in `onApiRegistrations` into a **mutable router object**
244
+ 3. `RPCHandler` is created **after** modules are loaded
245
+
246
+ ```typescript
247
+ // apps/server/src/rpc-router-registry.ts
248
+ export const rootRpcRouter = { ...appRouter }
249
+
250
+ export const wireModuleRpcRouters = (registrations: ApiRegistration[]) => {
251
+ for (const reg of registrations) {
252
+ if (reg.type === 'rpc-router') {
253
+ rootRpcRouter[reg.name] = reg.router
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ### Typing DI in Routers
260
+
261
+ Use a module-local interface to type `context.di.cradle`:
262
+
263
+ ```typescript
264
+ // In your module's router file
265
+ interface BillingCradle {
266
+ createInvoice: (input: CreateInvoiceInput) => Promise<Invoice>
267
+ }
268
+
269
+ export const invoicesRouter = {
270
+ create: protectedProcedure.input(createInvoiceSchema).handler(async ({ input, context }) => {
271
+ const { createInvoice } = context.di.cradle as BillingCradle
272
+ return createInvoice(input)
273
+ }),
274
+ }
275
+ ```
276
+
74
277
  ## Common Tasks
75
278
 
76
279
  ### Add a new API endpoint
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Server App
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -8,13 +8,69 @@ Express backend hosting oRPC API, Better-Auth authentication, and the Kuckit mod
8
8
 
9
9
  ## Key Files
10
10
 
11
- | File | Purpose |
12
- | ------------------- | ------------------------- |
13
- | `server.ts` | Entry point, bootstrap |
14
- | `container.ts` | DI container setup |
15
- | `config/modules.ts` | Module registration |
16
- | `rpc.ts` | oRPC handler setup |
17
- | `auth.ts` | Better-Auth configuration |
11
+ | File | Purpose |
12
+ | ------------------------ | -------------------------- |
13
+ | `server.ts` | Entry point, bootstrap |
14
+ | `container.ts` | DI container setup |
15
+ | `config/modules.ts` | Module registration |
16
+ | `rpc.ts` | oRPC handler setup |
17
+ | `rpc-router-registry.ts` | Mutable router for modules |
18
+ | `auth.ts` | Better-Auth configuration |
19
+
20
+ ## Module Loading Sequence
21
+
22
+ The server bootstraps in this order:
23
+
24
+ ```
25
+ 1. createKuckitContainer() - Core services (db, logger, cache, etc.)
26
+ 2. loadKuckitModules() with onApiRegistrations callback:
27
+ ├─► register() hooks run (DI bindings)
28
+ ├─► registerApi() hooks run (routers collected)
29
+ ├─► onApiRegistrations() - Wire routers to rootRpcRouter
30
+ └─► onBootstrap() hooks run (startup logic)
31
+ 3. setupRPC() - Create RPCHandler AFTER modules loaded
32
+ 4. Express listens
33
+ ```
34
+
35
+ ## Router Registry Pattern
36
+
37
+ oRPC's `RPCHandler` captures the router at construction time. Modules wire their routers into a **mutable object** before the handler is created:
38
+
39
+ ```typescript
40
+ // rpc-router-registry.ts
41
+ export const rootRpcRouter = { ...appRouter }
42
+
43
+ export const wireModuleRpcRouters = (registrations: ApiRegistration[]) => {
44
+ for (const reg of registrations) {
45
+ if (reg.type === 'rpc-router') {
46
+ rootRpcRouter[reg.name] = reg.router
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ```typescript
53
+ // server.ts
54
+ await loadKuckitModules({
55
+ container,
56
+ modules: getModuleSpecs(),
57
+ onApiRegistrations: (registrations) => {
58
+ wireModuleRpcRouters(registrations) // Wire BEFORE setupRPC()
59
+ },
60
+ })
61
+
62
+ setupRPC(app) // Now create handler with fully-wired router
63
+ ```
64
+
65
+ ## Per-Request Scoping
66
+
67
+ Each HTTP request gets a scoped container with:
68
+
69
+ - `requestId` - Unique request identifier
70
+ - `requestLogger` - Logger with request context
71
+ - `session` - Current user session (if authenticated)
72
+
73
+ Access scoped services in routers via `context.di.cradle`.
18
74
 
19
75
  ## Adding New Modules
20
76
 
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Web App
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -8,13 +8,15 @@ React frontend using TanStack Router, TanStack Query, and Kuckit client modules.
8
8
 
9
9
  ## Key Files
10
10
 
11
- | File | Purpose |
12
- | -------------------------------- | --------------------------------- |
13
- | `main.tsx` | App entry point |
14
- | `modules.client.ts` | Client module registration |
15
- | `providers/KuckitProvider.tsx` | Kuckit context setup |
16
- | `providers/ServicesProvider.tsx` | Services context (RPC, auth) |
17
- | `routes/` | TanStack Router file-based routes |
11
+ | File | Purpose |
12
+ | ---------------------------------- | --------------------------------- |
13
+ | `main.tsx` | App entry point |
14
+ | `modules.client.ts` | Client module registration |
15
+ | `providers/KuckitProvider.tsx` | Kuckit context setup |
16
+ | `providers/ServicesProvider.tsx` | Services context (RPC, auth) |
17
+ | `routes/` | TanStack Router file-based routes |
18
+ | `routes/$.tsx` | Catch-all for module routes |
19
+ | `components/KuckitModuleRoute.tsx` | Renders module-registered routes |
18
20
 
19
21
  ## Adding Routes
20
22
 
@@ -24,6 +26,78 @@ Create files in `src/routes/`:
24
26
  - `src/routes/users/$id.tsx` → `/users/:id`
25
27
  - `src/routes/settings/index.tsx` → `/settings`
26
28
 
29
+ **File-based routes take precedence** over module-registered routes.
30
+
31
+ ## Module Route Integration
32
+
33
+ Modules register routes via `defineKuckitClientModule`. These are rendered via a catch-all pattern:
34
+
35
+ 1. Module registers route in `routes` array
36
+ 2. `RouteRegistry` collects routes during `loadKuckitClientModules()`
37
+ 3. Catch-all route (`$.tsx`) matches any unhandled path
38
+ 4. `KuckitModuleRoute` looks up path in registry and renders the component
39
+
40
+ ```tsx
41
+ // routes/$.tsx - Catches module routes
42
+ import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
43
+
44
+ export function Route() {
45
+ return <KuckitModuleRoute />
46
+ }
47
+ ```
48
+
49
+ ## Context Providers
50
+
51
+ The app wraps content in Kuckit providers:
52
+
53
+ ```tsx
54
+ <ServicesProvider>
55
+ {' '}
56
+ {/* RPC client, auth */}
57
+ <KuckitProvider>
58
+ {' '}
59
+ {/* Nav, slots, routes registries */}
60
+ <KuckitRpcProvider>
61
+ {' '}
62
+ {/* RPC context for modules */}
63
+ {children}
64
+ </KuckitRpcProvider>
65
+ </KuckitProvider>
66
+ </ServicesProvider>
67
+ ```
68
+
69
+ **Available hooks:**
70
+
71
+ - `useRpc<T>()` - Get typed RPC client for API calls
72
+ - `useNavItems()` - Flat navigation items list
73
+ - `useNavTree()` - Hierarchical navigation tree
74
+ - `useSlot(name)` - Get components for a slot
75
+ - `useHasSlot(name)` - Check if slot has content
76
+
77
+ ## useRpc Hook
78
+
79
+ Module components must use `useRpc()` to access the API:
80
+
81
+ ```tsx
82
+ import { useRpc } from '@kuckit/sdk-react'
83
+ import { useQuery } from '@tanstack/react-query'
84
+
85
+ interface MyRpc {
86
+ items: { list: (input: {}) => Promise<Item[]> }
87
+ }
88
+
89
+ function MyComponent() {
90
+ const rpc = useRpc<MyRpc>()
91
+
92
+ const { data } = useQuery({
93
+ queryKey: ['items'],
94
+ queryFn: () => rpc.items.list({}),
95
+ })
96
+ }
97
+ ```
98
+
99
+ **Important**: Never use `import.meta.env.VITE_SERVER_URL` directly in module components - it's unavailable in external packages. The `useRpc` hook provides the correctly-configured client.
100
+
27
101
  ## Using Module UI Components
28
102
 
29
103
  ```tsx
@@ -0,0 +1,82 @@
1
+ import { useRouterState, Link, useNavigate } from '@tanstack/react-router'
2
+ import { useKuckit } from '@/providers/KuckitProvider'
3
+ import { useServices } from '@/providers/ServicesProvider'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ /**
7
+ * Dynamic route renderer for Kuckit module routes.
8
+ *
9
+ * This component looks up the current path in the RouteRegistry
10
+ * and renders the corresponding module component if found.
11
+ *
12
+ * For routes with meta.requiresAuth: true, redirects to /login if not authenticated.
13
+ */
14
+ export function KuckitModuleRoute() {
15
+ const { routeRegistry } = useKuckit()
16
+ const { authClient } = useServices()
17
+ const { location } = useRouterState()
18
+ const navigate = useNavigate()
19
+ const pathname = location.pathname
20
+
21
+ const [authChecked, setAuthChecked] = useState(false)
22
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
23
+
24
+ // Find matching route in registry
25
+ const routeDef = routeRegistry.getAll().find((r) => r.path === pathname)
26
+
27
+ // Check auth for routes with requiresAuth
28
+ useEffect(() => {
29
+ async function checkAuth() {
30
+ if (!routeDef) {
31
+ setAuthChecked(true)
32
+ return
33
+ }
34
+
35
+ // Check if auth is required
36
+ if (routeDef.meta?.requiresAuth) {
37
+ const session = await authClient.getSession()
38
+ if (!session.data) {
39
+ // Redirect to login with return URL
40
+ navigate({
41
+ to: '/login',
42
+ search: { redirect: pathname },
43
+ })
44
+ return
45
+ }
46
+ setIsAuthenticated(true)
47
+ }
48
+ setAuthChecked(true)
49
+ }
50
+
51
+ checkAuth()
52
+ }, [routeDef, pathname, navigate, authClient])
53
+
54
+ // Show loading while checking auth for protected routes
55
+ if (routeDef?.meta?.requiresAuth && !authChecked) {
56
+ return (
57
+ <div className="flex items-center justify-center min-h-screen">
58
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
59
+ </div>
60
+ )
61
+ }
62
+
63
+ // Protected route - waiting for redirect
64
+ if (routeDef?.meta?.requiresAuth && !isAuthenticated) {
65
+ return null
66
+ }
67
+
68
+ if (!routeDef) {
69
+ return (
70
+ <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
71
+ <h1 className="text-2xl font-bold">Page Not Found</h1>
72
+ <p className="text-muted-foreground">The page "{pathname}" could not be found.</p>
73
+ <Link to="/" className="text-primary hover:underline">
74
+ Go Home
75
+ </Link>
76
+ </div>
77
+ )
78
+ }
79
+
80
+ const Component = routeDef.component as React.ComponentType
81
+ return <Component />
82
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ClientModuleSpec } from '@kuckit/sdk-react'
2
+ import { kuckitClientModule as itemsClientModule } from '@__APP_NAME_KEBAB__/items-module/client'
2
3
 
3
4
  /**
4
5
  * Client modules configuration
@@ -7,9 +8,9 @@ import type { ClientModuleSpec } from '@kuckit/sdk-react'
7
8
  */
8
9
  export const getClientModuleSpecs = (): ClientModuleSpec[] => {
9
10
  const modules: ClientModuleSpec[] = [
10
- // KUCKIT_MODULES_START
11
- // Client modules will be added here
12
- // KUCKIT_MODULES_END
11
+ // KUCKIT_CLIENT_MODULES_START
12
+ { module: itemsClientModule },
13
+ // KUCKIT_CLIENT_MODULES_END
13
14
  ]
14
15
 
15
16
  return modules
@@ -0,0 +1,14 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
3
+
4
+ /**
5
+ * Catch-all route for KuckitModule routes at root level.
6
+ *
7
+ * This route matches any path not handled by other file-based routes
8
+ * and uses the KuckitModuleRoute component to look up and render
9
+ * the appropriate module component from the RouteRegistry.
10
+ */
11
+ // @ts-expect-error - Route types are generated when dev server runs
12
+ export const Route = createFileRoute('/$')({
13
+ component: KuckitModuleRoute,
14
+ })
@@ -3,7 +3,11 @@
3
3
  "compilerOptions": {
4
4
  "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
5
  "jsx": "react-jsx",
6
- "noEmit": true
6
+ "noEmit": true,
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@/*": ["./src/*"]
10
+ }
7
11
  },
8
12
  "include": ["src"]
9
13
  }
@@ -1,7 +1,13 @@
1
1
  import { defineConfig } from 'vite'
2
2
  import react from '@vitejs/plugin-react'
3
3
  import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
4
+ import path from 'path'
4
5
 
5
6
  export default defineConfig({
6
7
  plugins: [TanStackRouterVite(), react()],
8
+ resolve: {
9
+ alias: {
10
+ '@': path.resolve(__dirname, './src'),
11
+ },
12
+ },
7
13
  })
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - API Package
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -13,15 +13,54 @@ Shared API context, types, and procedure definitions for oRPC.
13
13
  | `context.ts` | Request context type definitions |
14
14
  | `index.ts` | Public exports |
15
15
 
16
- ## Usage
16
+ ## Procedure Types
17
17
 
18
18
  ```typescript
19
- import { type Context, protectedProcedure } from '@__APP_NAME_KEBAB__/api'
19
+ import { publicProcedure, protectedProcedure } from '@__APP_NAME_KEBAB__/api'
20
+
21
+ // Public - no authentication required
22
+ export const healthRouter = {
23
+ ping: publicProcedure.handler(() => 'pong'),
24
+ }
25
+
26
+ // Protected - requires authenticated user
27
+ export const itemsRouter = {
28
+ list: protectedProcedure.handler(async ({ context }) => {
29
+ const userId = context.session?.user?.id
30
+ // ...
31
+ }),
32
+ }
20
33
  ```
21
34
 
22
35
  ## Context Structure
23
36
 
24
37
  The API context provides:
25
38
 
26
- - `container` - Scoped DI container for the request
27
- - `user` - Authenticated user (when using `protectedProcedure`)
39
+ - `di` - Scoped DI container for the request
40
+ - `session` - Current user session (when using `protectedProcedure`)
41
+
42
+ ## Typing DI Access in Routers
43
+
44
+ Use a module-local interface to type `context.di.cradle`:
45
+
46
+ ```typescript
47
+ // In your module's router file
48
+ interface ItemsCradle {
49
+ itemRepository: ItemRepository
50
+ createItem: (input: CreateItemInput) => Promise<Item>
51
+ }
52
+
53
+ export const itemsRouter = {
54
+ create: protectedProcedure.input(createItemSchema).handler(async ({ input, context }) => {
55
+ const { createItem } = context.di.cradle as ItemsCradle
56
+ return createItem(input)
57
+ }),
58
+ }
59
+ ```
60
+
61
+ **Why module-local interfaces:**
62
+
63
+ - Type assertion stays within the module
64
+ - Module remains self-contained
65
+ - Server app doesn't need to know about module internals
66
+ - Scales to any number of modules without server changes
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Auth Package
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -36,6 +36,22 @@ await authClient.signOut()
36
36
  const session = await authClient.getSession()
37
37
  ```
38
38
 
39
+ ## Session in oRPC Context
40
+
41
+ The current user session is available in `protectedProcedure` handlers:
42
+
43
+ ```typescript
44
+ import { protectedProcedure } from '@__APP_NAME_KEBAB__/api'
45
+
46
+ export const myRouter = {
47
+ getProfile: protectedProcedure.handler(async ({ context }) => {
48
+ const userId = context.session?.user?.id
49
+ const userEmail = context.session?.user?.email
50
+ // ... use session data
51
+ }),
52
+ }
53
+ ```
54
+
39
55
  ## Configuration
40
56
 
41
57
  Auth requires these environment variables:
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Database Package
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -50,6 +50,21 @@ bun run db:studio
50
50
  bun run db:migrate
51
51
  ```
52
52
 
53
+ ## Module Schema Pattern
54
+
55
+ Modules can define their own schemas in `adapters/`:
56
+
57
+ ```typescript
58
+ // packages/items-module/src/adapters/items.schema.ts
59
+ export const itemsTable = pgTable('items', {
60
+ id: uuid('id').primaryKey().defaultRandom(),
61
+ userId: text('user_id').notNull(),
62
+ name: text('name').notNull(),
63
+ })
64
+ ```
65
+
66
+ The schema is imported by `drizzle.config.ts` for migration generation.
67
+
53
68
  ## Connection
54
69
 
55
70
  ```typescript
@@ -1,6 +1,9 @@
1
1
  # AGENTS.md - Items Module
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > **SDK Documentation**: For detailed module patterns, see the root [AGENTS.md](../../AGENTS.md) SDK Module System section
4
+ >
5
+ > - Server modules: `defineKuckitModule()` from `@kuckit/sdk`
6
+ > - Client modules: `defineKuckitClientModule()` from `@kuckit/sdk-react`
4
7
 
5
8
  ## Purpose
6
9
 
@@ -110,3 +113,98 @@ export const kuckitModule = defineKuckitModule({
110
113
  6. Implement use cases
111
114
  7. Create API router
112
115
  8. Register in server's `config/modules.ts`
116
+
117
+ ## Registration in Apps
118
+
119
+ ### Server Registration
120
+
121
+ ```typescript
122
+ // apps/server/src/config/modules.ts
123
+ import { kuckitModule as itemsModule } from '@__APP_NAME_KEBAB__/items-module'
124
+
125
+ export const modules = [itemsModule]
126
+ ```
127
+
128
+ ### Client Registration
129
+
130
+ ```typescript
131
+ // apps/web/src/modules.client.ts
132
+ import { kuckitClientModule as itemsClient } from '@__APP_NAME_KEBAB__/items-module/client'
133
+
134
+ export const clientModules = [{ module: itemsClient }]
135
+ ```
136
+
137
+ ## Client Module Pattern
138
+
139
+ The client module registers routes, navigation items, and slots:
140
+
141
+ ```typescript
142
+ // client-module.ts
143
+ import { defineKuckitClientModule } from '@kuckit/sdk-react'
144
+ import { ItemsPage } from './ui/ItemsPage'
145
+
146
+ export const kuckitClientModule = defineKuckitClientModule({
147
+ id: 'items',
148
+ displayName: 'Items',
149
+
150
+ routes: [
151
+ {
152
+ id: 'items-page',
153
+ path: '/items',
154
+ component: ItemsPage,
155
+ meta: { requiresAuth: true },
156
+ },
157
+ ],
158
+
159
+ navItems: [
160
+ {
161
+ id: 'items-nav',
162
+ label: 'Items',
163
+ href: '/items',
164
+ order: 20,
165
+ },
166
+ ],
167
+ })
168
+ ```
169
+
170
+ ## Using useRpc in Components
171
+
172
+ Module components access the API via the `useRpc` hook:
173
+
174
+ ```typescript
175
+ // ui/ItemsPage.tsx
176
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
177
+ import { useRpc } from '@kuckit/sdk-react'
178
+
179
+ interface ItemsRpc {
180
+ items: {
181
+ list: (input: Record<string, never>) => Promise<Item[]>
182
+ create: (input: { name: string; description?: string }) => Promise<Item>
183
+ delete: (input: { id: string }) => Promise<void>
184
+ }
185
+ }
186
+
187
+ export function ItemsPage() {
188
+ const rpc = useRpc<ItemsRpc>()
189
+ const queryClient = useQueryClient()
190
+
191
+ const { data: items = [], isLoading } = useQuery({
192
+ queryKey: ['items'],
193
+ queryFn: () => rpc.items.list({}),
194
+ })
195
+
196
+ const createMutation = useMutation({
197
+ mutationFn: (data: { name: string }) => rpc.items.create(data),
198
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
199
+ })
200
+
201
+ const deleteMutation = useMutation({
202
+ mutationFn: (id: string) => rpc.items.delete({ id }),
203
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
204
+ })
205
+
206
+ // ... render items list with create/delete handlers
207
+ }
208
+ ```
209
+
210
+ **Important**: Never use `import.meta.env` directly in module components - use `useRpc()` instead. Module packages are bundled separately and don't have access to the host app's environment variables.
@@ -1,4 +1,6 @@
1
- import { useState, useEffect } from 'react'
1
+ import { useState } from 'react'
2
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
+ import { useRpc } from '@kuckit/sdk-react'
2
4
 
3
5
  interface Item {
4
6
  id: string
@@ -8,85 +10,63 @@ interface Item {
8
10
  updatedAt: Date
9
11
  }
10
12
 
13
+ interface ItemsRpc {
14
+ items: {
15
+ list: (input: Record<string, never>) => Promise<Item[]>
16
+ create: (input: { name: string; description?: string }) => Promise<Item>
17
+ delete: (input: { id: string }) => Promise<void>
18
+ }
19
+ }
20
+
11
21
  /**
12
22
  * Items page component
13
- * Demonstrates CRUD operations using the items module
23
+ * Demonstrates CRUD operations using the items module with useRpc and TanStack Query
14
24
  */
15
25
  export function ItemsPage() {
16
- const [items, setItems] = useState<Item[]>([])
26
+ const rpc = useRpc<ItemsRpc>()
27
+ const queryClient = useQueryClient()
17
28
  const [newItemName, setNewItemName] = useState('')
18
29
  const [newItemDescription, setNewItemDescription] = useState('')
19
- const [loading, setLoading] = useState(true)
20
- const [error, setError] = useState<string | null>(null)
21
30
 
22
- const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000'
31
+ const {
32
+ data: items = [],
33
+ isLoading,
34
+ error,
35
+ } = useQuery({
36
+ queryKey: ['items'],
37
+ queryFn: () => rpc.items.list({}),
38
+ })
23
39
 
24
- // Fetch items on mount
25
- useEffect(() => {
26
- fetchItems()
27
- }, [])
40
+ const createMutation = useMutation({
41
+ mutationFn: (data: { name: string; description?: string }) => rpc.items.create(data),
42
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
43
+ })
28
44
 
29
- const fetchItems = async () => {
30
- try {
31
- setLoading(true)
32
- const response = await fetch(`${apiUrl}/rpc/items.list`, {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' },
35
- credentials: 'include',
36
- body: JSON.stringify({}),
37
- })
38
- if (response.ok) {
39
- const data = await response.json()
40
- setItems(data)
41
- }
42
- } catch {
43
- setError('Failed to load items')
44
- } finally {
45
- setLoading(false)
46
- }
47
- }
45
+ const deleteMutation = useMutation({
46
+ mutationFn: (id: string) => rpc.items.delete({ id }),
47
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
48
+ })
48
49
 
49
50
  const createItem = async (e: React.FormEvent) => {
50
51
  e.preventDefault()
51
52
  if (!newItemName.trim()) return
52
53
 
53
- try {
54
- const response = await fetch(`${apiUrl}/rpc/items.create`, {
55
- method: 'POST',
56
- headers: { 'Content-Type': 'application/json' },
57
- credentials: 'include',
58
- body: JSON.stringify({
59
- name: newItemName,
60
- description: newItemDescription || undefined,
61
- }),
62
- })
63
- if (response.ok) {
64
- setNewItemName('')
65
- setNewItemDescription('')
66
- fetchItems()
54
+ createMutation.mutate(
55
+ { name: newItemName, description: newItemDescription || undefined },
56
+ {
57
+ onSuccess: () => {
58
+ setNewItemName('')
59
+ setNewItemDescription('')
60
+ },
67
61
  }
68
- } catch {
69
- setError('Failed to create item')
70
- }
62
+ )
71
63
  }
72
64
 
73
- const deleteItem = async (id: string) => {
74
- try {
75
- const response = await fetch(`${apiUrl}/rpc/items.delete`, {
76
- method: 'POST',
77
- headers: { 'Content-Type': 'application/json' },
78
- credentials: 'include',
79
- body: JSON.stringify({ id }),
80
- })
81
- if (response.ok) {
82
- fetchItems()
83
- }
84
- } catch {
85
- setError('Failed to delete item')
86
- }
65
+ const deleteItem = (id: string) => {
66
+ deleteMutation.mutate(id)
87
67
  }
88
68
 
89
- if (loading) {
69
+ if (isLoading) {
90
70
  return <div style={{ padding: '2rem' }}>Loading items...</div>
91
71
  }
92
72
 
@@ -94,12 +74,9 @@ export function ItemsPage() {
94
74
  <div style={{ padding: '2rem', fontFamily: 'system-ui', maxWidth: '600px', margin: '0 auto' }}>
95
75
  <h1>Items</h1>
96
76
 
97
- {error && (
77
+ {(error || createMutation.error || deleteMutation.error) && (
98
78
  <div style={{ color: 'red', marginBottom: '1rem' }}>
99
- {error}
100
- <button onClick={() => setError(null)} style={{ marginLeft: '1rem' }}>
101
- Dismiss
102
- </button>
79
+ {error?.message || createMutation.error?.message || deleteMutation.error?.message}
103
80
  </div>
104
81
  )}
105
82
 
@@ -120,8 +97,12 @@ export function ItemsPage() {
120
97
  onChange={(e) => setNewItemDescription(e.target.value)}
121
98
  style={{ padding: '0.5rem', fontSize: '1rem' }}
122
99
  />
123
- <button type="submit" style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}>
124
- Add Item
100
+ <button
101
+ type="submit"
102
+ disabled={createMutation.isPending}
103
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
104
+ >
105
+ {createMutation.isPending ? 'Adding...' : 'Add Item'}
125
106
  </button>
126
107
  </div>
127
108
  </form>
@@ -148,6 +129,7 @@ export function ItemsPage() {
148
129
  </div>
149
130
  <button
150
131
  onClick={() => deleteItem(item.id)}
132
+ disabled={deleteMutation.isPending}
151
133
  style={{ cursor: 'pointer', color: 'red', background: 'none', border: 'none' }}
152
134
  >
153
135
  Delete