create-theokit 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +1 -1
- package/templates/api-only/README.md.tmpl +78 -0
- package/templates/api-only/package.json.tmpl +6 -1
- package/templates/api-only/server/routes/webhooks/echo.ts +34 -0
- package/templates/dashboard/README.md.tmpl +76 -0
- package/templates/dashboard/package.json.tmpl +6 -1
- package/templates/dashboard/server/crons/cleanup-conversations.ts +51 -0
- package/templates/default/README.md.tmpl +2 -0
- package/templates/default/package.json.tmpl +6 -1
- package/templates/default/server/crons/cleanup-conversations.ts +51 -0
- package/templates/postgres/README.md.tmpl +83 -0
- package/templates/postgres/package.json.tmpl +6 -1
- package/templates/postgres/server/jobs/log-message.ts +27 -0
- package/templates/saas/README.md.tmpl +103 -0
- package/templates/saas/package.json.tmpl +6 -1
- package/templates/saas/server/routes/agent.ts +32 -4
- package/templates/saas/server/routes/billing/stripe-webhook.ts +49 -0
package/package.json
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
TheoKit API-only project. Backend routes with Zod validation + typed responses — no frontend bundle, no React.
|
|
4
|
+
|
|
5
|
+
> 📚 **Full docs:** https://docs.theokit.dev
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Set your provider key if you wire an agent route later
|
|
11
|
+
echo 'OPENROUTER_API_KEY=sk-or-v1-...' > .env
|
|
12
|
+
|
|
13
|
+
# 2. Boot the dev server
|
|
14
|
+
npx theokit dev
|
|
15
|
+
|
|
16
|
+
# 3. Probe the health route
|
|
17
|
+
curl http://localhost:3000/api/health
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You should see `{"status":"ok"}`. The server is now serving the routes under `server/routes/`.
|
|
21
|
+
|
|
22
|
+
## Templates
|
|
23
|
+
|
|
24
|
+
- **default** — TheoUI chat composer + agent route.
|
|
25
|
+
- **dashboard** — nested layouts + sidebar.
|
|
26
|
+
- **api-only** (this one) — server routes without React.
|
|
27
|
+
- **postgres** — Drizzle ORM + migrations.
|
|
28
|
+
- **saas** — full app with auth, billing, sessions.
|
|
29
|
+
|
|
30
|
+
## What the framework auto-loads
|
|
31
|
+
|
|
32
|
+
- **`.env` → `process.env`**. Edit `.env`; restart the dev server.
|
|
33
|
+
- **`.theo/` build output cleanup** on every `theokit build`.
|
|
34
|
+
- **Route discovery** — every `server/routes/*.ts` becomes a wired endpoint.
|
|
35
|
+
|
|
36
|
+
## Project structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
server/
|
|
40
|
+
├── routes/
|
|
41
|
+
│ ├── health.ts GET /api/health — returns {status:"ok"}
|
|
42
|
+
│ └── users.ts CRUD /api/users — Zod-validated body
|
|
43
|
+
theo.config.ts Framework config
|
|
44
|
+
.env Secrets — never committed (.gitignore)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Sample requests
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Health check
|
|
51
|
+
curl http://localhost:3000/api/health
|
|
52
|
+
|
|
53
|
+
# Create a user (POST with JSON body)
|
|
54
|
+
curl -X POST http://localhost:3000/api/users \
|
|
55
|
+
-H 'Content-Type: application/json' \
|
|
56
|
+
-d '{"name":"Alice","email":"alice@example.com"}'
|
|
57
|
+
|
|
58
|
+
# List users
|
|
59
|
+
curl http://localhost:3000/api/users
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Common commands
|
|
63
|
+
|
|
64
|
+
| Command | What it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `npx theokit dev` | Dev server with HMR + structured logs |
|
|
67
|
+
| `npx theokit build` | Production build → `.theo/` |
|
|
68
|
+
| `npx theokit start` | Serve the production build |
|
|
69
|
+
| `npx theokit routes` | List all routes detected |
|
|
70
|
+
| `npm run typecheck` | TypeScript strict check (no emit) |
|
|
71
|
+
|
|
72
|
+
## Add a new route
|
|
73
|
+
|
|
74
|
+
Drop a `.ts` file in `server/routes/`. Use `defineRoute` from `theokit/server` for Zod-validated handlers, or export `GET`/`POST` directly.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
Apply your own. The TheoKit framework is Apache-2.0.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"theokit": "^0.1.0-alpha.
|
|
13
|
+
"theokit": "^0.1.0-alpha.14",
|
|
14
14
|
"react": "^19.0.0",
|
|
15
15
|
"react-dom": "^19.0.0"
|
|
16
16
|
},
|
|
@@ -18,5 +18,10 @@
|
|
|
18
18
|
"typescript": "^5.7.0",
|
|
19
19
|
"@types/react": "^19.0.0",
|
|
20
20
|
"@types/react-dom": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"pnpm": {
|
|
23
|
+
"onlyBuiltDependencies": [
|
|
24
|
+
"esbuild"
|
|
25
|
+
]
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineWebhook } from 'theokit/server'
|
|
2
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Echo webhook — demonstrates `defineWebhook` HMAC-SHA256 pattern
|
|
7
|
+
* without depending on an external provider (Stripe, GitHub, etc.).
|
|
8
|
+
*
|
|
9
|
+
* Self-test:
|
|
10
|
+
* SECRET=$(openssl rand -base64 32)
|
|
11
|
+
* echo -n '{"message":"hi"}' | openssl dgst -sha256 -hmac "$SECRET"
|
|
12
|
+
* curl -X POST localhost:3000/api/webhooks/echo \
|
|
13
|
+
* -H "x-echo-signature: <hex from above>" \
|
|
14
|
+
* -H "Content-Type: application/json" \
|
|
15
|
+
* -d '{"message":"hi"}'
|
|
16
|
+
*/
|
|
17
|
+
const ECHO_SECRET = process.env.ECHO_WEBHOOK_SECRET ?? ''
|
|
18
|
+
|
|
19
|
+
export const POST = defineWebhook({
|
|
20
|
+
verify: ({ rawBody, headers }) => {
|
|
21
|
+
if (ECHO_SECRET === '') return false
|
|
22
|
+
const sig = headers.get('x-echo-signature') ?? ''
|
|
23
|
+
const expected = createHmac('sha256', ECHO_SECRET).update(rawBody).digest('hex')
|
|
24
|
+
try {
|
|
25
|
+
return timingSafeEqual(Buffer.from(sig, 'utf-8'), Buffer.from(expected, 'utf-8'))
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
inputSchema: z.object({ message: z.string() }),
|
|
31
|
+
handler: async ({ input }) => {
|
|
32
|
+
return Response.json({ echoed: input.message, timestamp: new Date().toISOString() })
|
|
33
|
+
},
|
|
34
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
TheoKit dashboard project. Build the app your agent lives in — with nested layouts and a sidebar wired from day one.
|
|
4
|
+
|
|
5
|
+
> 📚 **Full docs:** https://docs.theokit.dev
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Set your provider key (OpenRouter recommended — one key, any model)
|
|
11
|
+
echo 'OPENROUTER_API_KEY=sk-or-v1-...' > .env
|
|
12
|
+
|
|
13
|
+
# 2. Boot the dev server
|
|
14
|
+
npx theokit dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open the printed URL. The default surface is a dashboard shell with sidebar nav + content area, ready to host your agent panels.
|
|
18
|
+
|
|
19
|
+
## Templates
|
|
20
|
+
|
|
21
|
+
- **default** — TheoUI chat composer + agent route.
|
|
22
|
+
- **dashboard** (this one) — nested layouts + sidebar nav.
|
|
23
|
+
- **api-only** — server routes without React.
|
|
24
|
+
- **postgres** — Drizzle ORM + migrations.
|
|
25
|
+
- **saas** — full app with auth, billing, sessions.
|
|
26
|
+
|
|
27
|
+
## What the framework auto-loads
|
|
28
|
+
|
|
29
|
+
- **`.env` → `process.env`**. Edit `.env`; restart the dev server.
|
|
30
|
+
- **`.theo/` build output cleanup** on every `theokit build`.
|
|
31
|
+
- **Tailwind + `@usetheo/ui` styling** auto-configured for the TheoUI surface.
|
|
32
|
+
|
|
33
|
+
## Project structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
app/ Frontend (file-based routing with nested layouts)
|
|
37
|
+
├── layout.tsx root wrapper — TheoUI provider + theme
|
|
38
|
+
├── page.tsx / — dashboard home
|
|
39
|
+
├── dashboard/
|
|
40
|
+
│ ├── layout.tsx /dashboard/* — sidebar shell
|
|
41
|
+
│ └── page.tsx /dashboard — primary panel
|
|
42
|
+
server/ Backend (explicit routes)
|
|
43
|
+
├── routes/
|
|
44
|
+
│ └── health.ts GET /api/health
|
|
45
|
+
theo.config.ts Framework config
|
|
46
|
+
tailwind.config.ts Tailwind theme tokens
|
|
47
|
+
.env Secrets — never committed
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Common commands
|
|
51
|
+
|
|
52
|
+
| Command | What it does |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `npx theokit dev` | Dev server with HMR + devtools overlay |
|
|
55
|
+
| `npx theokit build` | Production build → `.theo/` |
|
|
56
|
+
| `npx theokit start` | Serve the production build |
|
|
57
|
+
| `npx theokit check` | Lint for upgrade-readiness |
|
|
58
|
+
| `npx theokit routes` | List all routes + actions detected |
|
|
59
|
+
| `npm run typecheck` | TypeScript strict check (no emit) |
|
|
60
|
+
|
|
61
|
+
## Add a new panel
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mkdir -p app/dashboard/billing
|
|
65
|
+
cat > app/dashboard/billing/page.tsx <<'EOF'
|
|
66
|
+
export default function BillingPage() {
|
|
67
|
+
return <h2>Billing</h2>
|
|
68
|
+
}
|
|
69
|
+
EOF
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The route appears at `/dashboard/billing` after HMR.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
Apply your own. The TheoKit framework is Apache-2.0.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"theokit": "^0.1.0-alpha.
|
|
13
|
+
"theokit": "^0.1.0-alpha.14",
|
|
14
14
|
"react": "^19.0.0",
|
|
15
15
|
"react-dom": "^19.0.0"
|
|
16
16
|
},
|
|
@@ -18,5 +18,10 @@
|
|
|
18
18
|
"typescript": "^5.7.0",
|
|
19
19
|
"@types/react": "^19.0.0",
|
|
20
20
|
"@types/react-dom": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"pnpm": {
|
|
23
|
+
"onlyBuiltDependencies": [
|
|
24
|
+
"esbuild"
|
|
25
|
+
]
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineCron } from 'theokit/server/cron'
|
|
2
|
+
import { readdir, stat, rm } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Daily GC of stale conversation transcripts.
|
|
7
|
+
*
|
|
8
|
+
* The `@usetheo/sdk` Agent persists chat history under
|
|
9
|
+
* `.theokit/agents/<agentId>/messages.jsonl`. With no TTL the directory
|
|
10
|
+
* grows unbounded — production foot-gun. This cron removes any agent
|
|
11
|
+
* directory whose `messages.jsonl` hasn't been touched in 30 days.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_AGE_DAYS = 30
|
|
14
|
+
const AGENTS_DIR = '.theokit/agents'
|
|
15
|
+
|
|
16
|
+
export default defineCron({
|
|
17
|
+
name: 'cleanup-conversations',
|
|
18
|
+
schedule: '0 4 * * *', // Daily 04:00 UTC
|
|
19
|
+
handler: async ({ log }) => {
|
|
20
|
+
const root = resolve(process.cwd(), AGENTS_DIR)
|
|
21
|
+
const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1000
|
|
22
|
+
let removed = 0
|
|
23
|
+
let kept = 0
|
|
24
|
+
let entries: Awaited<ReturnType<typeof readdir>>
|
|
25
|
+
try {
|
|
26
|
+
entries = await readdir(root, { withFileTypes: true })
|
|
27
|
+
} catch {
|
|
28
|
+
log.info({ msg: 'No agents dir yet — first run', dir: root })
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isDirectory()) continue
|
|
33
|
+
const agentDir = join(root, entry.name)
|
|
34
|
+
const messagesFile = join(agentDir, 'messages.jsonl')
|
|
35
|
+
try {
|
|
36
|
+
const s = await stat(messagesFile)
|
|
37
|
+
if (s.mtimeMs < cutoff) {
|
|
38
|
+
await rm(agentDir, { recursive: true, force: true })
|
|
39
|
+
removed++
|
|
40
|
+
} else {
|
|
41
|
+
kept++
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// messages.jsonl missing → orphan dir, remove
|
|
45
|
+
await rm(agentDir, { recursive: true, force: true }).catch(() => {})
|
|
46
|
+
removed++
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
log.info({ msg: 'cleanup-conversations complete', removed, kept, maxAgeDays: MAX_AGE_DAYS })
|
|
50
|
+
},
|
|
51
|
+
})
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"theokit": "^0.1.0-alpha.
|
|
13
|
+
"theokit": "^0.1.0-alpha.14",
|
|
14
14
|
"@usetheo/sdk": "^1.2.0",
|
|
15
15
|
"@usetheo/ui": "^0.12.0-next.0",
|
|
16
16
|
"lucide-react": "^0.469.0",
|
|
@@ -25,5 +25,10 @@
|
|
|
25
25
|
"@types/react-dom": "^19.0.0",
|
|
26
26
|
"tailwindcss": "^4.0.0",
|
|
27
27
|
"@tailwindcss/vite": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"pnpm": {
|
|
30
|
+
"onlyBuiltDependencies": [
|
|
31
|
+
"esbuild"
|
|
32
|
+
]
|
|
28
33
|
}
|
|
29
34
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineCron } from 'theokit/server/cron'
|
|
2
|
+
import { readdir, stat, rm } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Daily GC of stale conversation transcripts.
|
|
7
|
+
*
|
|
8
|
+
* The `@usetheo/sdk` Agent persists chat history under
|
|
9
|
+
* `.theokit/agents/<agentId>/messages.jsonl`. With no TTL the directory
|
|
10
|
+
* grows unbounded — production foot-gun. This cron removes any agent
|
|
11
|
+
* directory whose `messages.jsonl` hasn't been touched in 30 days.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_AGE_DAYS = 30
|
|
14
|
+
const AGENTS_DIR = '.theokit/agents'
|
|
15
|
+
|
|
16
|
+
export default defineCron({
|
|
17
|
+
name: 'cleanup-conversations',
|
|
18
|
+
schedule: '0 4 * * *', // Daily 04:00 UTC
|
|
19
|
+
handler: async ({ log }) => {
|
|
20
|
+
const root = resolve(process.cwd(), AGENTS_DIR)
|
|
21
|
+
const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1000
|
|
22
|
+
let removed = 0
|
|
23
|
+
let kept = 0
|
|
24
|
+
let entries: Awaited<ReturnType<typeof readdir>>
|
|
25
|
+
try {
|
|
26
|
+
entries = await readdir(root, { withFileTypes: true })
|
|
27
|
+
} catch {
|
|
28
|
+
log.info({ msg: 'No agents dir yet — first run', dir: root })
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isDirectory()) continue
|
|
33
|
+
const agentDir = join(root, entry.name)
|
|
34
|
+
const messagesFile = join(agentDir, 'messages.jsonl')
|
|
35
|
+
try {
|
|
36
|
+
const s = await stat(messagesFile)
|
|
37
|
+
if (s.mtimeMs < cutoff) {
|
|
38
|
+
await rm(agentDir, { recursive: true, force: true })
|
|
39
|
+
removed++
|
|
40
|
+
} else {
|
|
41
|
+
kept++
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// messages.jsonl missing → orphan dir, remove
|
|
45
|
+
await rm(agentDir, { recursive: true, force: true }).catch(() => {})
|
|
46
|
+
removed++
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
log.info({ msg: 'cleanup-conversations complete', removed, kept, maxAgeDays: MAX_AGE_DAYS })
|
|
50
|
+
},
|
|
51
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
TheoKit project with Postgres + Drizzle ORM wired. Schema-first, migration-aware, typed end-to-end.
|
|
4
|
+
|
|
5
|
+
> 📚 **Full docs:** https://docs.theokit.dev
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 0. Provision Postgres
|
|
11
|
+
# Option A — local docker (one-liner):
|
|
12
|
+
docker run --name pg -e POSTGRES_PASSWORD=dev -p 5432:5432 -d postgres:16
|
|
13
|
+
# Option B — hosted: neon.tech, supabase.com, fly.io
|
|
14
|
+
|
|
15
|
+
# 1. Set env
|
|
16
|
+
cat > .env <<'EOF'
|
|
17
|
+
DATABASE_URL=postgres://postgres:dev@localhost:5432/postgres
|
|
18
|
+
OPENROUTER_API_KEY=sk-or-v1-...
|
|
19
|
+
EOF
|
|
20
|
+
|
|
21
|
+
# 2. Generate + apply migrations
|
|
22
|
+
pnpm db:generate
|
|
23
|
+
pnpm db:migrate
|
|
24
|
+
|
|
25
|
+
# 3. Boot the dev server
|
|
26
|
+
npx theokit dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Open the printed URL. The default surface includes a sample users API backed by Postgres.
|
|
30
|
+
|
|
31
|
+
## Templates
|
|
32
|
+
|
|
33
|
+
- **default** — TheoUI chat composer + agent route.
|
|
34
|
+
- **dashboard** — nested layouts + sidebar.
|
|
35
|
+
- **api-only** — server routes without React.
|
|
36
|
+
- **postgres** (this one) — Drizzle ORM + migrations.
|
|
37
|
+
- **saas** — full app with auth, billing, sessions.
|
|
38
|
+
|
|
39
|
+
## What the framework auto-loads
|
|
40
|
+
|
|
41
|
+
- **`.env` → `process.env`**. `DATABASE_URL` must be set before `pnpm db:migrate`.
|
|
42
|
+
- **`.theo/` build output cleanup** on every `theokit build`.
|
|
43
|
+
- **Drizzle schema** under `db/schema.ts` drives migrations + typed query builder.
|
|
44
|
+
|
|
45
|
+
## Project structure
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
app/ Frontend
|
|
49
|
+
├── page.tsx / — sample UI
|
|
50
|
+
server/
|
|
51
|
+
├── routes/
|
|
52
|
+
│ ├── health.ts GET /api/health
|
|
53
|
+
│ └── users.ts CRUD /api/users — backed by db.users
|
|
54
|
+
db/
|
|
55
|
+
├── schema.ts Drizzle schema (tables + relations)
|
|
56
|
+
├── client.ts Drizzle client (used by routes)
|
|
57
|
+
└── migrations/ Generated SQL files (committed)
|
|
58
|
+
drizzle.config.ts Drizzle CLI config
|
|
59
|
+
theo.config.ts Framework config
|
|
60
|
+
.env Secrets — never committed
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Common commands
|
|
64
|
+
|
|
65
|
+
| Command | What it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `npx theokit dev` | Dev server with HMR |
|
|
68
|
+
| `npx theokit build` | Production build |
|
|
69
|
+
| `npx theokit start` | Serve production build |
|
|
70
|
+
| `pnpm db:generate` | Generate SQL migration from `db/schema.ts` |
|
|
71
|
+
| `pnpm db:migrate` | Apply pending migrations to `DATABASE_URL` |
|
|
72
|
+
| `pnpm db:studio` | Open Drizzle Studio UI |
|
|
73
|
+
| `npm run typecheck` | TypeScript strict check |
|
|
74
|
+
|
|
75
|
+
## Troubleshooting
|
|
76
|
+
|
|
77
|
+
- **`pnpm db:migrate` fails with "ECONNREFUSED"** → check `DATABASE_URL` host:port + Postgres is running (`docker ps` or hosted dashboard).
|
|
78
|
+
- **`relation "users" does not exist`** → you forgot `pnpm db:migrate`. Run it.
|
|
79
|
+
- **Schema change not reflected** → `pnpm db:generate` first, then `pnpm db:migrate`.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
Apply your own. The TheoKit framework is Apache-2.0.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"db:studio": "drizzle-kit studio"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"theokit": "^0.1.0-alpha.
|
|
17
|
+
"theokit": "^0.1.0-alpha.14",
|
|
18
18
|
"react": "^19.0.0",
|
|
19
19
|
"react-dom": "^19.0.0",
|
|
20
20
|
"drizzle-orm": "^0.45.0",
|
|
@@ -26,5 +26,10 @@
|
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
28
|
"drizzle-kit": "^0.31.0"
|
|
29
|
+
},
|
|
30
|
+
"pnpm": {
|
|
31
|
+
"onlyBuiltDependencies": [
|
|
32
|
+
"esbuild"
|
|
33
|
+
]
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineJob } from 'theokit/server/jobs'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { appendFile, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { resolve, dirname } from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Background job demonstrating `defineJob` + `ctx.queue.enqueue` pattern.
|
|
8
|
+
*
|
|
9
|
+
* Triggered from `server/routes/users.ts` POST handler via:
|
|
10
|
+
* await ctx.queue.enqueue('log-message', { userId, message })
|
|
11
|
+
*
|
|
12
|
+
* Per ADR-0003 (transactional outbox), enqueue is deferred until the
|
|
13
|
+
* route handler commits successfully — handler throws → 0 jobs dispatched.
|
|
14
|
+
*/
|
|
15
|
+
export default defineJob({
|
|
16
|
+
name: 'log-message',
|
|
17
|
+
input: z.object({ userId: z.string(), message: z.string() }),
|
|
18
|
+
handler: async ({ input, log }) => {
|
|
19
|
+
// v1.1 EC-9: anchor path to process.cwd() — handler CWD may differ from
|
|
20
|
+
// project root when running via external job runner.
|
|
21
|
+
const auditPath = resolve(process.cwd(), '.theo/audit.log')
|
|
22
|
+
await mkdir(dirname(auditPath), { recursive: true })
|
|
23
|
+
const line = `${new Date().toISOString()} user=${input.userId} msg=${input.message}\n`
|
|
24
|
+
await appendFile(auditPath, line)
|
|
25
|
+
log.info({ msg: 'audit logged', userId: input.userId, path: auditPath })
|
|
26
|
+
},
|
|
27
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
TheoKit SaaS template — auth, sessions, billing-ready, and an agent route. The full stack for shipping an account-aware product on day one.
|
|
4
|
+
|
|
5
|
+
> 📚 **Full docs:** https://docs.theokit.dev
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 0. Provision Postgres (sessions + users)
|
|
11
|
+
docker run --name pg -e POSTGRES_PASSWORD=dev -p 5432:5432 -d postgres:16
|
|
12
|
+
# Or use a hosted Postgres (neon.tech / supabase.com)
|
|
13
|
+
|
|
14
|
+
# 1. Set env (generate a strong session secret)
|
|
15
|
+
cat > .env <<EOF
|
|
16
|
+
DATABASE_URL=postgres://postgres:dev@localhost:5432/postgres
|
|
17
|
+
SESSION_SECRET=$(openssl rand -base64 32)
|
|
18
|
+
OPENROUTER_API_KEY=sk-or-v1-...
|
|
19
|
+
EOF
|
|
20
|
+
|
|
21
|
+
# 2. Migrate the schema
|
|
22
|
+
pnpm db:generate
|
|
23
|
+
pnpm db:migrate
|
|
24
|
+
|
|
25
|
+
# 3. Boot the dev server
|
|
26
|
+
npx theokit dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Sample auth flow
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Register
|
|
33
|
+
curl -X POST http://localhost:3000/api/register \
|
|
34
|
+
-H 'Content-Type: application/json' \
|
|
35
|
+
-d '{"email":"alice@example.com","password":"strong-passphrase"}'
|
|
36
|
+
|
|
37
|
+
# Login (saves session cookie)
|
|
38
|
+
curl -X POST http://localhost:3000/api/login \
|
|
39
|
+
-H 'Content-Type: application/json' \
|
|
40
|
+
-d '{"email":"alice@example.com","password":"strong-passphrase"}' \
|
|
41
|
+
-c cookies.txt
|
|
42
|
+
|
|
43
|
+
# Authenticated request
|
|
44
|
+
curl http://localhost:3000/api/me -b cookies.txt
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Templates
|
|
48
|
+
|
|
49
|
+
- **default** — TheoUI chat composer + agent route.
|
|
50
|
+
- **dashboard** — nested layouts + sidebar.
|
|
51
|
+
- **api-only** — server routes without React.
|
|
52
|
+
- **postgres** — Drizzle ORM + migrations.
|
|
53
|
+
- **saas** (this one) — full app with auth, billing, sessions.
|
|
54
|
+
|
|
55
|
+
## What the framework auto-loads
|
|
56
|
+
|
|
57
|
+
- **`.env` → `process.env`**. `SESSION_SECRET`, `DATABASE_URL`, `OPENROUTER_API_KEY` all required.
|
|
58
|
+
- **Encrypted sessions** (AES-256-GCM) via `SESSION_SECRET`.
|
|
59
|
+
- **`.theo/` build output cleanup** on every `theokit build`.
|
|
60
|
+
|
|
61
|
+
## Project structure
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
app/ Frontend
|
|
65
|
+
├── page.tsx / — landing
|
|
66
|
+
server/
|
|
67
|
+
├── routes/
|
|
68
|
+
│ ├── login.ts POST /api/login — sets session cookie
|
|
69
|
+
│ ├── logout.ts POST /api/logout — clears session
|
|
70
|
+
│ ├── me.ts GET /api/me — requireAuth() guarded
|
|
71
|
+
│ └── agent.ts POST /api/agent — agent SSE, requireAuth()
|
|
72
|
+
db/
|
|
73
|
+
├── schema.ts users + sessions tables (Drizzle)
|
|
74
|
+
└── migrations/ generated SQL (committed)
|
|
75
|
+
drizzle.config.ts Drizzle config
|
|
76
|
+
theo.config.ts Framework config
|
|
77
|
+
.env Secrets — never committed
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Common commands
|
|
81
|
+
|
|
82
|
+
| Command | What it does |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `npx theokit dev` | Dev server with HMR + auth |
|
|
85
|
+
| `npx theokit build` | Production build |
|
|
86
|
+
| `npx theokit start` | Serve production build |
|
|
87
|
+
| `pnpm db:generate` | Generate SQL migration |
|
|
88
|
+
| `pnpm db:migrate` | Apply migrations |
|
|
89
|
+
| `npm run typecheck` | TypeScript strict check |
|
|
90
|
+
|
|
91
|
+
## Adding billing
|
|
92
|
+
|
|
93
|
+
Stripe is the canonical path — add `STRIPE_SECRET_KEY` to `.env`, mount `server/routes/billing/webhook.ts` via `defineWebhook`, and wire plan checks into `requireAuth()`.
|
|
94
|
+
|
|
95
|
+
## Troubleshooting
|
|
96
|
+
|
|
97
|
+
- **`SESSION_SECRET must be at least 32 bytes`** → regenerate with `openssl rand -base64 32`.
|
|
98
|
+
- **`relation "users" does not exist`** → run `pnpm db:migrate` first.
|
|
99
|
+
- **`401 Unauthorized` on `/api/me`** → login first; cookie must be sent (`-b cookies.txt`).
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
Apply your own. The TheoKit framework is Apache-2.0.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"db:studio": "drizzle-kit studio"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"theokit": "^0.1.0-alpha.
|
|
17
|
+
"theokit": "^0.1.0-alpha.14",
|
|
18
18
|
"@usetheo/ui": "^0.12.0-next.0",
|
|
19
19
|
"react": "^19.0.0",
|
|
20
20
|
"react-dom": "^19.0.0",
|
|
@@ -28,5 +28,10 @@
|
|
|
28
28
|
"@types/react": "^19.0.0",
|
|
29
29
|
"@types/react-dom": "^19.0.0",
|
|
30
30
|
"drizzle-kit": "^0.31.0"
|
|
31
|
+
},
|
|
32
|
+
"pnpm": {
|
|
33
|
+
"onlyBuiltDependencies": [
|
|
34
|
+
"esbuild"
|
|
35
|
+
]
|
|
31
36
|
}
|
|
32
37
|
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { defineAgentEndpoint, requireAuth, type AgentEvent } from 'theokit/server'
|
|
2
|
+
import { trackAgentRun } from 'theokit/server/cost'
|
|
2
3
|
import type { RequestContext } from '../context.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Protected agent endpoint. `requireAuth` fires BEFORE the stream starts;
|
|
6
7
|
* unauthorized requests get 401 immediately — no SSE bytes leak.
|
|
7
8
|
*
|
|
9
|
+
* Observability: wraps the run with `trackAgentRun` to surface per-user
|
|
10
|
+
* cost + token usage to the configured `UsageStorageAdapter` (configure via
|
|
11
|
+
* `theo.config.ts > cost.storage`). Also feeds the devtools `Agents` tab
|
|
12
|
+
* (when running in dev).
|
|
13
|
+
*
|
|
14
|
+
* NOTE: `costUsd: 0` is a v1 stub. Pricing table integration is a
|
|
15
|
+
* `@usetheo/sdk` follow-up (R0.5.11). Devtools tab renders "$0.0000" —
|
|
16
|
+
* indicates "cost tracking not yet calibrated for this model".
|
|
17
|
+
*
|
|
8
18
|
* Replace the mock generator with your LLM provider call.
|
|
9
19
|
*/
|
|
10
20
|
export const POST = defineAgentEndpoint<{ message: string }, RequestContext>({
|
|
@@ -12,10 +22,28 @@ export const POST = defineAgentEndpoint<{ message: string }, RequestContext>({
|
|
|
12
22
|
requireAuth(ctx.session)
|
|
13
23
|
const body = (await request.json()) as { message?: string }
|
|
14
24
|
const msg = body.message ?? ''
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
try {
|
|
26
|
+
yield {
|
|
27
|
+
type: 'message',
|
|
28
|
+
content: `Hello ${ctx.session.email}, you said: "${msg}"`,
|
|
29
|
+
}
|
|
30
|
+
yield { type: 'message', content: '(Replace this mock with your LLM.)' }
|
|
31
|
+
} finally {
|
|
32
|
+
// Always emit observability — even on stream error / abort.
|
|
33
|
+
// `storage` resolved from theo.config.ts > cost.storage (undefined =
|
|
34
|
+
// no-op; configure to enable persistence + devtools tab visibility).
|
|
35
|
+
// To enable persistent cost tracking: wire `cost: { storage }` into
|
|
36
|
+
// `theo.config.ts` and forward via context. Demo passes `undefined`
|
|
37
|
+
// (no-op storage; still fires devtools dispatcher in dev mode).
|
|
38
|
+
await trackAgentRun(
|
|
39
|
+
{
|
|
40
|
+
userId: ctx.session.email,
|
|
41
|
+
model: 'mock/echo',
|
|
42
|
+
tokens: { input: msg.length, output: 0 }, // crude — real impl uses tokenizer
|
|
43
|
+
costUsd: 0, // v1 stub
|
|
44
|
+
},
|
|
45
|
+
{ storage: undefined },
|
|
46
|
+
)
|
|
18
47
|
}
|
|
19
|
-
yield { type: 'message', content: '(Replace this mock with your LLM.)' }
|
|
20
48
|
},
|
|
21
49
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineWebhook } from 'theokit/server'
|
|
2
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stripe webhook receiver.
|
|
7
|
+
*
|
|
8
|
+
* Verifies `Stripe-Signature` header per Stripe's documented HMAC-SHA256
|
|
9
|
+
* scheme (https://stripe.com/docs/webhooks/signatures). Real impl would
|
|
10
|
+
* handle `checkout.session.completed`, `invoice.paid`, etc.
|
|
11
|
+
*
|
|
12
|
+
* Setup:
|
|
13
|
+
* 1. Create webhook endpoint in Stripe Dashboard pointing to /api/billing/stripe-webhook
|
|
14
|
+
* 2. Copy signing secret → `.env` STRIPE_WEBHOOK_SECRET
|
|
15
|
+
* 3. Test locally: `stripe listen --forward-to localhost:3000/api/billing/stripe-webhook`
|
|
16
|
+
*/
|
|
17
|
+
const STRIPE_SECRET = process.env.STRIPE_WEBHOOK_SECRET ?? ''
|
|
18
|
+
|
|
19
|
+
export const POST = defineWebhook({
|
|
20
|
+
verify: ({ rawBody, headers }) => {
|
|
21
|
+
if (STRIPE_SECRET === '') return false
|
|
22
|
+
const sigHeader = headers.get('stripe-signature') ?? ''
|
|
23
|
+
// Stripe format: `t=<unix-ts>,v1=<hash>` (potentially also v0)
|
|
24
|
+
const parts: Record<string, string> = {}
|
|
25
|
+
for (const pair of sigHeader.split(',')) {
|
|
26
|
+
const [k, v] = pair.split('=')
|
|
27
|
+
if (k && v) parts[k.trim()] = v.trim()
|
|
28
|
+
}
|
|
29
|
+
const t = parts['t']
|
|
30
|
+
const v1 = parts['v1']
|
|
31
|
+
if (!t || !v1) return false
|
|
32
|
+
const signedPayload = `${t}.${rawBody}`
|
|
33
|
+
const expected = createHmac('sha256', STRIPE_SECRET).update(signedPayload).digest('hex')
|
|
34
|
+
try {
|
|
35
|
+
return timingSafeEqual(Buffer.from(v1, 'utf-8'), Buffer.from(expected, 'utf-8'))
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
inputSchema: z.object({ type: z.string(), data: z.unknown() }),
|
|
41
|
+
handler: async ({ input, log }) => {
|
|
42
|
+
log.info({ msg: 'stripe webhook received', type: input.type })
|
|
43
|
+
// TODO: dispatch by input.type:
|
|
44
|
+
// - 'checkout.session.completed' → activate subscription
|
|
45
|
+
// - 'invoice.paid' → extend access
|
|
46
|
+
// - 'invoice.payment_failed' → notify user + retry plan
|
|
47
|
+
return Response.json({ received: true })
|
|
48
|
+
},
|
|
49
|
+
})
|