create-theokit 1.0.6 → 1.0.8

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.
Files changed (32) hide show
  1. package/dist/cli.js +5 -0
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/default/AGENTS.md +41 -51
  5. package/templates/default/CLAUDE.md +25 -0
  6. package/templates/default/README.md.tmpl +53 -51
  7. package/templates/default/_gitignore +31 -0
  8. package/templates/default/app/globals.css +36 -0
  9. package/templates/default/app/page.tsx +72 -166
  10. package/templates/default/app.ts +24 -0
  11. package/templates/default/dot-claude/rules/theokit-conventions.md +36 -0
  12. package/templates/default/dot-claude/settings.json +23 -0
  13. package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +105 -0
  14. package/templates/default/dot-claude/skills/theokit-config/SKILL.md +128 -0
  15. package/templates/default/dot-claude/skills/theokit-database/SKILL.md +112 -0
  16. package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +89 -0
  17. package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +89 -0
  18. package/templates/default/drizzle.config.ts +10 -0
  19. package/templates/default/eslint.config.mjs +5 -1
  20. package/templates/default/package.json.tmpl +13 -3
  21. package/templates/default/public/index.html +70 -0
  22. package/templates/default/server/agents/assistant.agent.ts +6 -2
  23. package/templates/default/server/controllers/tasks.controller.ts +5 -13
  24. package/templates/default/server/db/index.ts +21 -0
  25. package/templates/default/server/db/schema.ts +13 -0
  26. package/templates/default/server/db/seed.ts +29 -0
  27. package/templates/default/server/index.ts +2 -18
  28. package/templates/default/server/routes/tasks/[id].ts +36 -0
  29. package/templates/default/server/routes/tasks/index.ts +20 -0
  30. package/templates/default/server/toolboxes/task.tools.ts +2 -1
  31. package/templates/default/tests/tasks.test.ts +10 -0
  32. package/templates/default/theo.config.ts +0 -6
@@ -220,6 +220,7 @@ code, pre { font-family: var(--font-mono); }
220
220
  border: 1px solid var(--border);
221
221
  border-radius: 12px;
222
222
  padding: 24px;
223
+ width: 100%;
223
224
  }
224
225
 
225
226
  .card h2 {
@@ -314,6 +315,41 @@ tr.done td { opacity: 0.4; text-decoration: line-through; }
314
315
  }
315
316
  .chat-bar input:focus { border-color: var(--accent); }
316
317
 
318
+ /* ─── Features grid ─────────────────────────────────── */
319
+
320
+ .features {
321
+ margin-top: 32px;
322
+ }
323
+
324
+ .feature {
325
+ padding: 20px;
326
+ border: 1px solid var(--border);
327
+ border-radius: 12px;
328
+ background: var(--card);
329
+ }
330
+
331
+ .feature h3 {
332
+ font-size: 14px;
333
+ font-weight: 600;
334
+ margin-bottom: 6px;
335
+ font-family: var(--font-mono);
336
+ color: var(--accent);
337
+ }
338
+
339
+ .feature p {
340
+ font-size: 13px;
341
+ line-height: 1.5;
342
+ color: var(--text-secondary);
343
+ }
344
+
345
+ .feature code {
346
+ background: var(--bg);
347
+ border: 1px solid var(--border);
348
+ padding: 1px 5px;
349
+ border-radius: 4px;
350
+ font-size: 12px;
351
+ }
352
+
317
353
  /* ─── Footer ────────────────────────────────────────── */
318
354
 
319
355
  .footer {
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState, useEffect, useCallback, useRef, type FormEvent } from 'react'
3
+ import { useState, useEffect, useCallback, type FormEvent } from 'react'
4
4
 
5
5
  interface Task {
6
6
  id: number
@@ -8,30 +8,12 @@ interface Task {
8
8
  priority: 'high' | 'medium' | 'low'
9
9
  done: boolean
10
10
  }
11
- type Role = '' | 'user' | 'admin'
12
- interface ChatMsg {
13
- role: 'user' | 'agent' | 'tool' | 'system' | 'error'
14
- text: string
15
- }
16
11
 
17
12
  export default function Page() {
18
13
  const [tasks, setTasks] = useState<Task[]>([])
19
- const [role, setRole] = useState<Role>('user')
20
14
  const [title, setTitle] = useState('')
21
15
  const [priority, setPriority] = useState<Task['priority']>('medium')
22
16
  const [formError, setFormError] = useState('')
23
- const [chat, setChat] = useState<ChatMsg[]>([
24
- { role: 'system', text: 'Ask me to list, create, or complete tasks...' },
25
- ])
26
- const [chatInput, setChatInput] = useState('')
27
- const [chatBusy, setChatBusy] = useState(false)
28
- const chatRef = useRef<HTMLDivElement>(null)
29
-
30
- const hdrs = useCallback((): Record<string, string> => {
31
- const h: Record<string, string> = { 'Content-Type': 'application/json' }
32
- if (role) h['x-role'] = role
33
- return h
34
- }, [role])
35
17
 
36
18
  const loadTasks = useCallback(async () => {
37
19
  const res = await fetch('/api/tasks')
@@ -48,13 +30,9 @@ export default function Page() {
48
30
  if (!title.trim()) return
49
31
  const res = await fetch('/api/tasks', {
50
32
  method: 'POST',
51
- headers: hdrs(),
33
+ headers: { 'Content-Type': 'application/json' },
52
34
  body: JSON.stringify({ title, priority }),
53
35
  })
54
- if (res.status === 403) {
55
- setFormError('403 — Need User role')
56
- return
57
- }
58
36
  if (!res.ok) {
59
37
  const b = await res.json()
60
38
  setFormError(b.error?.issues?.[0]?.message ?? `Error ${res.status}`)
@@ -64,65 +42,6 @@ export default function Page() {
64
42
  loadTasks()
65
43
  }
66
44
 
67
- const sendChat = async () => {
68
- const msg = chatInput.trim()
69
- if (!msg || chatBusy) return
70
- setChatInput('')
71
- setChat((c) => [...c, { role: 'user', text: msg }])
72
- setChatBusy(true)
73
- try {
74
- const res = await fetch('/api/agents/assistant/chat', {
75
- method: 'POST',
76
- headers: hdrs(),
77
- body: JSON.stringify({ message: msg, sessionId: 'session-' + Date.now() }),
78
- })
79
- if (res.status === 403) {
80
- setChat((c) => [...c, { role: 'error', text: '403 — Need User role' }])
81
- return
82
- }
83
- const reader = res.body?.getReader()
84
- if (!reader) return
85
- const decoder = new TextDecoder()
86
- let buf = '',
87
- agentText = ''
88
- while (true) {
89
- const { done, value } = await reader.read()
90
- if (done) break
91
- buf += decoder.decode(value, { stream: true })
92
- const lines = buf.split('\n')
93
- buf = lines.pop() ?? ''
94
- for (const line of lines) {
95
- if (!line.startsWith('data: ')) continue
96
- try {
97
- const ev = JSON.parse(line.slice(6))
98
- if (ev.type === 'text_delta') agentText += ev.content
99
- else if (ev.type === 'tool_call')
100
- setChat((c) => [...c, { role: 'tool', text: `🔧 ${ev.toolName}` }])
101
- else if (ev.type === 'tool_result')
102
- setChat((c) => [...c, { role: 'tool', text: `✅ ${(ev.output ?? '').slice(0, 80)}` }])
103
- else if (ev.type === 'error')
104
- setChat((c) => [...c, { role: 'error', text: ev.message }])
105
- } catch {
106
- /* partial */
107
- }
108
- }
109
- }
110
- if (agentText) setChat((c) => [...c, { role: 'agent', text: agentText }])
111
- loadTasks()
112
- } catch (err) {
113
- setChat((c) => [
114
- ...c,
115
- { role: 'error', text: `Error: ${err instanceof Error ? err.message : String(err)}` },
116
- ])
117
- } finally {
118
- setChatBusy(false)
119
- }
120
- }
121
-
122
- useEffect(() => {
123
- chatRef.current?.scrollTo(0, chatRef.current.scrollHeight)
124
- }, [chat])
125
-
126
45
  return (
127
46
  <div className="page">
128
47
  <div className="main">
@@ -150,94 +69,81 @@ export default function Page() {
150
69
  </a>
151
70
  </nav>
152
71
  <p className="hint">
153
- Edit <code>app/page.tsx</code> to get started.
72
+ Edit <code>app/page.tsx</code> to get started. Changes hot-reload instantly.
154
73
  </p>
155
74
  </header>
156
75
 
157
- {/* Role */}
158
- <div className="role-bar">
159
- <label htmlFor="role">Role:</label>
160
- <select id="role" value={role} onChange={(e) => setRole(e.target.value as Role)}>
161
- <option value="">None (public)</option>
162
- <option value="user">User</option>
163
- <option value="admin">Admin</option>
164
- </select>
165
- </div>
166
-
167
- {/* Content */}
168
- <div className="grid">
169
- <section className="card">
170
- <h2>
171
- Tasks <span className="badge">@Controller</span>
172
- </h2>
173
- <table>
174
- <thead>
175
- <tr>
176
- <th>Task</th>
177
- <th>Priority</th>
178
- <th>Status</th>
76
+ {/* Tasks */}
77
+ <section className="card">
78
+ <h2>
79
+ Tasks <span className="badge">defineRoute + Drizzle</span>
80
+ </h2>
81
+ <table>
82
+ <thead>
83
+ <tr>
84
+ <th>Task</th>
85
+ <th>Priority</th>
86
+ <th>Status</th>
87
+ </tr>
88
+ </thead>
89
+ <tbody>
90
+ {tasks.map((t) => (
91
+ <tr key={t.id} className={t.done ? 'done' : ''}>
92
+ <td>
93
+ {t.done ? '✅ ' : '○ '}
94
+ {t.title}
95
+ </td>
96
+ <td>
97
+ <span className={`prio prio-${t.priority}`}>{t.priority}</span>
98
+ </td>
99
+ <td>{t.done ? 'Done' : 'To do'}</td>
179
100
  </tr>
180
- </thead>
181
- <tbody>
182
- {tasks.map((t) => (
183
- <tr key={t.id} className={t.done ? 'done' : ''}>
184
- <td>
185
- {t.done ? '✅ ' : '○ '}
186
- {t.title}
187
- </td>
188
- <td>
189
- <span className={`prio prio-${t.priority}`}>{t.priority}</span>
190
- </td>
191
- <td>{t.done ? 'Done' : 'To do'}</td>
192
- </tr>
193
- ))}
194
- </tbody>
195
- </table>
196
- <form onSubmit={createTask} className="create-bar">
197
- <input
198
- value={title}
199
- onChange={(e) => setTitle(e.target.value)}
200
- placeholder="New task..."
201
- required
202
- minLength={3}
203
- />
204
- <select
205
- value={priority}
206
- onChange={(e) => setPriority(e.target.value as Task['priority'])}
207
- >
208
- <option value="medium">Medium</option>
209
- <option value="high">High</option>
210
- <option value="low">Low</option>
211
- </select>
212
- <button type="submit">Add</button>
213
- </form>
214
- {formError && <p className="error">{formError}</p>}
215
- </section>
216
-
217
- <section className="card">
218
- <h2>
219
- AI Assistant <span className="badge badge-ai">@Agent + SSE</span>
220
- </h2>
221
- <div ref={chatRef} className="chat-box">
222
- {chat.map((m, i) => (
223
- <div key={i} className={`msg ${m.role}`}>
224
- {m.role === 'user' ? `You: ${m.text}` : m.text}
225
- </div>
226
101
  ))}
227
- </div>
228
- <div className="chat-bar">
229
- <input
230
- value={chatInput}
231
- onChange={(e) => setChatInput(e.target.value)}
232
- onKeyDown={(e) => e.key === 'Enter' && sendChat()}
233
- placeholder="Message the AI assistant..."
234
- disabled={chatBusy}
235
- />
236
- <button type="button" onClick={sendChat} disabled={chatBusy}>
237
- Send
238
- </button>
239
- </div>
240
- </section>
102
+ </tbody>
103
+ </table>
104
+ <form onSubmit={createTask} className="create-bar">
105
+ <input
106
+ value={title}
107
+ onChange={(e) => setTitle(e.target.value)}
108
+ placeholder="New task..."
109
+ required
110
+ minLength={3}
111
+ />
112
+ <select
113
+ value={priority}
114
+ onChange={(e) => setPriority(e.target.value as Task['priority'])}
115
+ >
116
+ <option value="medium">Medium</option>
117
+ <option value="high">High</option>
118
+ <option value="low">Low</option>
119
+ </select>
120
+ <button type="submit">Add</button>
121
+ </form>
122
+ {formError && <p className="error">{formError}</p>}
123
+ </section>
124
+
125
+ {/* Features */}
126
+ <div className="grid features">
127
+ <div className="feature">
128
+ <h3>defineRoute</h3>
129
+ <p>
130
+ Typed API routes with Zod validation. See <code>server/routes/tasks/</code>
131
+ </p>
132
+ </div>
133
+ <div className="feature">
134
+ <h3>Drizzle + SQLite</h3>
135
+ <p>
136
+ Type-safe database with zero config. Schema in <code>server/db/schema.ts</code>
137
+ </p>
138
+ </div>
139
+ <div className="feature">
140
+ <h3>@Agent + @Tool</h3>
141
+ <p>AI agents with SSE streaming, budget control, and human-in-the-loop approval.</p>
142
+ </div>
143
+ <div className="feature">
144
+ <h3>React + Vite</h3>
145
+ <p>File-based routing, HMR, SSR streaming. Edit and see changes instantly.</p>
146
+ </div>
241
147
  </div>
242
148
 
243
149
  {/* Footer */}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * TheoKit App — convention over configuration.
3
+ *
4
+ * "The framework that reduces noise for humans + AI."
5
+ *
6
+ * Backend classes registered in server/index.ts (one barrel, like Rails).
7
+ * Routes inferred from class names. Zero manual wiring.
8
+ */
9
+ import 'reflect-metadata'
10
+ import { readFileSync } from 'node:fs'
11
+ import { TheoApp } from '@theokit/http/app'
12
+ import { TasksController, AssistantAgent, TaskTools } from './server/index.js'
13
+
14
+ let html: string | undefined
15
+ try { html = readFileSync(new URL('./public/index.html', import.meta.url), 'utf-8') } catch { /* no frontend */ }
16
+
17
+ const app = await TheoApp.create({
18
+ controllers: [TasksController],
19
+ agents: [AssistantAgent],
20
+ providers: [TaskTools],
21
+ html,
22
+ })
23
+
24
+ await app.listen(3000)
@@ -0,0 +1,36 @@
1
+ # TheoKit Conventions
2
+
3
+ ## Imports
4
+
5
+ - Use `theokit/server/define` for defineRoute, defineAction, defineWebSocket
6
+ - Use `theokit/client` for theoFetch, createAppClient
7
+ - Use `theokit/server/auth` for session/auth APIs
8
+ - NEVER import from `theokit/dist/...` or `theokit/src/...`
9
+ - NEVER import internal modules: `theokit/core`, `theokit/vite-plugin`, `theokit/adapters/*`
10
+
11
+ ## Validation
12
+
13
+ - Zod is the single source of truth for types and validation
14
+ - Define schema ONCE with `z.object(...)`, derive types with `z.infer<>`
15
+ - NEVER duplicate a Zod schema as a manual TypeScript interface
16
+ - NEVER parse request body manually — use `body:` in defineRoute
17
+
18
+ ## Routes
19
+
20
+ - File at `server/routes/tasks/[id].ts` maps to `/api/tasks/:id`
21
+ - Export HTTP method handlers: `export const GET = defineRoute({...})`
22
+ - Use `params: z.object({...})` for URL params, `body:` for request body
23
+ - Use `status: 201` for creation responses, not manual `res.status()`
24
+
25
+ ## Types
26
+
27
+ - No `any` in production code
28
+ - No `@ts-ignore` or `@ts-expect-error`
29
+ - No `as` type assertions — use Zod schemas or type guards
30
+
31
+ ## Database
32
+
33
+ - Schema lives in `server/db/schema.ts` (Drizzle ORM)
34
+ - Connection in `server/db/index.ts`
35
+ - Seeds in `server/db/seed.ts`
36
+ - Use `npx drizzle-kit push` for dev, `npx drizzle-kit generate` for prod migrations
@@ -0,0 +1,23 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run *)",
5
+ "Bash(npx theokit *)",
6
+ "Bash(npx vitest *)",
7
+ "Bash(npx tsc *)",
8
+ "Bash(npx eslint *)",
9
+ "Bash(npx drizzle-kit *)",
10
+ "Bash(git status)",
11
+ "Bash(git diff *)",
12
+ "Bash(git log *)"
13
+ ],
14
+ "deny": [
15
+ "Read(.env*)",
16
+ "Read(**/.env*)",
17
+ "Bash(sudo *)",
18
+ "Bash(rm -rf *)",
19
+ "Bash(git push --force *)",
20
+ "Bash(git reset --hard *)"
21
+ ]
22
+ }
23
+ }
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: theokit-agents
3
+ description: TheoKit agent/LLM integration — @Agent, @Tool, @Toolbox decorators, streaming, memory
4
+ user-invocable: false
5
+ paths:
6
+ - "**/*agent*"
7
+ - "**/*Agent*"
8
+ - "**/*tool*"
9
+ - "**/*Tool*"
10
+ - "**/*toolbox*"
11
+ - "**/*Toolbox*"
12
+ ---
13
+
14
+ # TheoKit Agents & Tools
15
+
16
+ ## @Agent Decorator
17
+
18
+ ```typescript
19
+ import { Agent, MainLoop, Hook, Memory, Budget } from '@theokit/agents'
20
+
21
+ @Agent({
22
+ model: 'openai/gpt-4o-mini', // Required: LLM model
23
+ systemPrompt: 'You are a helpful task assistant.',
24
+ })
25
+ @Memory({ provider: 'built-in', scope: 'per-user' }) // Optional
26
+ @Budget({ maxCostUsd: 1.00, window: 'daily' }) // Optional
27
+ export class AssistantAgent {
28
+ @MainLoop({ strategy: 'react', maxIterations: 5 })
29
+ async run() {
30
+ // Framework handles the LLM loop
31
+ }
32
+
33
+ @Hook('before:llm-call')
34
+ async onBeforeLLM(ctx) {
35
+ // Intercept before each LLM call
36
+ }
37
+ }
38
+ ```
39
+
40
+ Convention: `AssistantAgent` class name maps to `GET/POST /api/agents/assistant`.
41
+
42
+ ## @Tool Decorator
43
+
44
+ ```typescript
45
+ import { Toolbox, Tool } from '@theokit/agents'
46
+ import { z } from 'zod'
47
+
48
+ @Toolbox()
49
+ export class TaskTools {
50
+ @Tool({
51
+ name: 'list_tasks',
52
+ description: 'List all tasks, optionally filtered by status',
53
+ input: z.object({
54
+ done: z.boolean().optional(),
55
+ }),
56
+ })
57
+ async listTasks({ done }: { done?: boolean }) {
58
+ const all = db.select().from(tasks).all()
59
+ return done !== undefined ? all.filter(t => t.done === done) : all
60
+ }
61
+
62
+ @Tool({
63
+ name: 'create_task',
64
+ description: 'Create a new task with a title',
65
+ input: z.object({
66
+ title: z.string().min(1),
67
+ }),
68
+ })
69
+ async createTask({ title }: { title: string }) {
70
+ return db.insert(tasks).values({ title }).returning().get()
71
+ }
72
+ }
73
+ ```
74
+
75
+ ## Frontend — useAgentStream
76
+
77
+ ```typescript
78
+ import { useAgentStream } from 'theokit/client'
79
+
80
+ function ChatUI() {
81
+ const { status, events, send } = useAgentStream('/api/agents/assistant')
82
+
83
+ return (
84
+ <div>
85
+ {events.map(e => <p key={e.id}>{e.content}</p>)}
86
+ <button onClick={() => send({ message: 'Hello' })}>Send</button>
87
+ </div>
88
+ )
89
+ }
90
+ ```
91
+
92
+ ## Rules
93
+
94
+ - Tool `name` and `description` are ALWAYS explicit — never inferred from method names (G4)
95
+ - Tool `input` uses Zod schema — same pattern as defineRoute
96
+ - `@UseGuards()` works on agents (shared with HTTP pipeline)
97
+ - `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings, not enforced at runtime)
98
+ - Agent runtime is `@theokit/sdk` — NEVER call LLM APIs directly via fetch
99
+
100
+ ## Anti-patterns
101
+
102
+ - NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent + @Tool
103
+ - NEVER reimplement tool calling loop — the SDK handles it
104
+ - NEVER store conversations manually — use @Memory
105
+ - NEVER infer tool capability from method name — always provide explicit `name` + `description`
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: theokit-config
3
+ description: TheoKit configuration — defineConfig, plugins, security, storage, agents, build targets
4
+ user-invocable: false
5
+ paths:
6
+ - "theo.config*"
7
+ - "**/*config*"
8
+ ---
9
+
10
+ # TheoKit Configuration
11
+
12
+ ## theo.config.ts
13
+
14
+ ```typescript
15
+ import { defineConfig } from 'theokit'
16
+
17
+ export default defineConfig({
18
+ // Basic
19
+ name: 'my-app', // DNS-1123 format (lowercase + hyphens)
20
+ port: 3000, // Dev + production port
21
+
22
+ // SSR (default: false)
23
+ ssr: false,
24
+
25
+ // Security (defaults are secure)
26
+ security: {
27
+ csrf: true, // CSRF protection (default: true)
28
+ csp: 'report-only', // Content Security Policy
29
+ },
30
+
31
+ // Agent runtime
32
+ agents: {
33
+ maxRegistries: 100,
34
+ registry: {
35
+ maxAgents: 100,
36
+ idleTimeoutMs: 30 * 60_000,
37
+ },
38
+ },
39
+
40
+ // DevTools overlay (dev only)
41
+ devtools: true,
42
+
43
+ // Plugins
44
+ plugins: [],
45
+ })
46
+ ```
47
+
48
+ ## Common Configuration Patterns
49
+
50
+ ### Adding CORS
51
+
52
+ ```typescript
53
+ import { defineConfig } from 'theokit'
54
+
55
+ export default defineConfig({
56
+ // CORS is handled by the framework — configure in route-level or globally
57
+ security: {
58
+ cors: {
59
+ origin: ['http://localhost:3000', 'https://myapp.com'],
60
+ credentials: true,
61
+ },
62
+ },
63
+ })
64
+ ```
65
+
66
+ ### Storage (Postgres + Redis)
67
+
68
+ ```typescript
69
+ export default defineConfig({
70
+ storage: {
71
+ postgres: [{ url: process.env.DATABASE_URL }],
72
+ redis: [{ url: process.env.REDIS_URL }],
73
+ },
74
+ })
75
+ ```
76
+
77
+ ### Rate Limiting
78
+
79
+ ```typescript
80
+ export default defineConfig({
81
+ rateLimit: {
82
+ global: { max: 100, windowMs: 60_000 },
83
+ },
84
+ })
85
+ ```
86
+
87
+ ### OpenAPI Generation
88
+
89
+ ```typescript
90
+ export default defineConfig({
91
+ openapi: {
92
+ title: 'My App API',
93
+ version: '1.0.0',
94
+ outDir: 'docs/api',
95
+ },
96
+ })
97
+ ```
98
+
99
+ ## CLI Commands
100
+
101
+ ```bash
102
+ npx theokit dev # Start dev server with HMR
103
+ npx theokit build # Build for Node.js
104
+ npx theokit build --target=node # Explicit target
105
+ npx theokit start # Run production build
106
+ npx theokit routes # List all discovered endpoints
107
+ npx theokit generate route tasks # Scaffold a new route
108
+ npx theokit generate resource posts title:string # Scaffold CRUD resource
109
+ npx theokit db migrate # Run database migrations
110
+ npx theokit db seed # Seed database
111
+ ```
112
+
113
+ ## Environment Variables
114
+
115
+ - `PORT` — Server port (overrides config)
116
+ - `HOST` — Server host
117
+ - `NODE_ENV` — `development` | `production`
118
+ - `DATABASE_URL` — Postgres connection string (when using postgres storage)
119
+ - `REDIS_URL` — Redis connection string (when using redis storage)
120
+
121
+ Env vars are loaded from `.env` (dev) and `.env.production` (build). NEVER commit `.env` files.
122
+
123
+ ## Anti-patterns
124
+
125
+ - NEVER hardcode secrets in theo.config.ts — use environment variables
126
+ - NEVER set `security.csrf: false` in production
127
+ - NEVER use `ssr: true` without understanding hydration (start with `false`)
128
+ - NEVER add plugins that don't match `defineTheoPlugin` interface