create-katajs 0.1.0

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/index.js +568 -0
  4. package/dist/templates/api/.github/workflows/deploy.yml +26 -0
  5. package/dist/templates/api/README.md +95 -0
  6. package/dist/templates/api/_dev.vars.example +3 -0
  7. package/dist/templates/api/_gitignore +11 -0
  8. package/dist/templates/api/drizzle.config.ts +10 -0
  9. package/dist/templates/api/package.json +32 -0
  10. package/dist/templates/api/scripts/graph.ts +19 -0
  11. package/dist/templates/api/scripts/modules.ts +7 -0
  12. package/dist/templates/api/src/app.ts +39 -0
  13. package/dist/templates/api/src/db/schema.ts +11 -0
  14. package/dist/templates/api/src/index.ts +6 -0
  15. package/dist/templates/api/src/modules/posts/index.ts +31 -0
  16. package/dist/templates/api/src/modules/posts/posts.errors.ts +15 -0
  17. package/dist/templates/api/src/modules/posts/posts.repository.ts +22 -0
  18. package/dist/templates/api/src/modules/posts/posts.routes.ts +20 -0
  19. package/dist/templates/api/src/modules/posts/posts.schema.ts +12 -0
  20. package/dist/templates/api/src/modules/posts/posts.service.ts +26 -0
  21. package/dist/templates/api/src/types.d.ts +31 -0
  22. package/dist/templates/api/tsconfig.json +22 -0
  23. package/dist/templates/api/wrangler.jsonc +35 -0
  24. package/dist/templates/auth-snippets/api/src/db/auth-schema.ts +53 -0
  25. package/dist/templates/auth-snippets/api/src/modules/auth/auth.config.ts +30 -0
  26. package/dist/templates/auth-snippets/api/src/modules/auth/auth.errors.ts +10 -0
  27. package/dist/templates/auth-snippets/api/src/modules/auth/auth.middleware.ts +37 -0
  28. package/dist/templates/auth-snippets/api/src/modules/auth/auth.routes.ts +7 -0
  29. package/dist/templates/auth-snippets/api/src/modules/auth/index.ts +28 -0
  30. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.errors.ts +10 -0
  31. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.middleware.ts +37 -0
  32. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.routes.ts +7 -0
  33. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/index.ts +29 -0
  34. package/dist/templates/auth-snippets/monorepo/packages/auth/package.json +22 -0
  35. package/dist/templates/auth-snippets/monorepo/packages/auth/src/auth.ts +40 -0
  36. package/dist/templates/auth-snippets/monorepo/packages/auth/src/index.ts +2 -0
  37. package/dist/templates/auth-snippets/monorepo/packages/auth/tsconfig.json +4 -0
  38. package/dist/templates/auth-snippets/monorepo/packages/db/src/auth-schema.ts +53 -0
  39. package/dist/templates/monorepo/.github/workflows/deploy.yml +39 -0
  40. package/dist/templates/monorepo/README.md +91 -0
  41. package/dist/templates/monorepo/_gitignore +12 -0
  42. package/dist/templates/monorepo/apps/api/_dev.vars.example +7 -0
  43. package/dist/templates/monorepo/apps/api/_gitignore +7 -0
  44. package/dist/templates/monorepo/apps/api/package.json +36 -0
  45. package/dist/templates/monorepo/apps/api/scripts/graph.ts +19 -0
  46. package/dist/templates/monorepo/apps/api/scripts/modules.ts +7 -0
  47. package/dist/templates/monorepo/apps/api/src/app.ts +42 -0
  48. package/dist/templates/monorepo/apps/api/src/index.ts +7 -0
  49. package/dist/templates/monorepo/apps/api/src/modules/posts/index.ts +31 -0
  50. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.errors.ts +15 -0
  51. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.repository.ts +22 -0
  52. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.routes.ts +20 -0
  53. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.schema.ts +13 -0
  54. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.service.ts +26 -0
  55. package/dist/templates/monorepo/apps/api/src/types.d.ts +29 -0
  56. package/dist/templates/monorepo/apps/api/tsconfig.json +7 -0
  57. package/dist/templates/monorepo/apps/api/wrangler.jsonc +34 -0
  58. package/dist/templates/monorepo/package.json +24 -0
  59. package/dist/templates/monorepo/packages/api-client/package.json +20 -0
  60. package/dist/templates/monorepo/packages/api-client/src/index.ts +43 -0
  61. package/dist/templates/monorepo/packages/api-client/tsconfig.json +4 -0
  62. package/dist/templates/monorepo/packages/db/drizzle.config.ts +12 -0
  63. package/dist/templates/monorepo/packages/db/package.json +26 -0
  64. package/dist/templates/monorepo/packages/db/src/index.ts +13 -0
  65. package/dist/templates/monorepo/packages/db/src/schema.ts +12 -0
  66. package/dist/templates/monorepo/packages/db/tsconfig.json +4 -0
  67. package/dist/templates/monorepo/pnpm-workspace.yaml +3 -0
  68. package/dist/templates/monorepo/tsconfig.base.json +18 -0
  69. package/dist/templates/monorepo/turbo.json +19 -0
  70. package/dist/templates/monorepo-worker/apps/worker/_dev.vars.example +4 -0
  71. package/dist/templates/monorepo-worker/apps/worker/_gitignore +6 -0
  72. package/dist/templates/monorepo-worker/apps/worker/package.json +29 -0
  73. package/dist/templates/monorepo-worker/apps/worker/src/app.ts +27 -0
  74. package/dist/templates/monorepo-worker/apps/worker/src/index.ts +6 -0
  75. package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.consumer.ts +23 -0
  76. package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.service.ts +18 -0
  77. package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/index.ts +24 -0
  78. package/dist/templates/monorepo-worker/apps/worker/src/types.d.ts +19 -0
  79. package/dist/templates/monorepo-worker/apps/worker/tsconfig.json +7 -0
  80. package/dist/templates/monorepo-worker/apps/worker/wrangler.jsonc +28 -0
  81. package/package.json +54 -0
@@ -0,0 +1,95 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Scaffolded with [`create-katajs`](https://github.com/yasr/katajs).
4
+
5
+ ## Setup
6
+
7
+ ```sh
8
+ pnpm install
9
+ ```
10
+
11
+ Spin up a local Postgres for development (Docker example):
12
+
13
+ ```sh
14
+ docker run -d --name {{PROJECT_NAME}}-pg \
15
+ -e POSTGRES_PASSWORD=postgres \
16
+ -e POSTGRES_DB={{PROJECT_NAME}}_dev \
17
+ -p 5432:5432 \
18
+ postgres:16
19
+ ```
20
+
21
+ Copy `.dev.vars.example` to `.dev.vars` (for `wrangler dev`).
22
+
23
+ ## Develop
24
+
25
+ ```sh
26
+ pnpm db:generate # generate Drizzle migrations after schema changes
27
+ pnpm db:migrate # apply migrations to your dev Postgres
28
+ pnpm dev # wrangler dev
29
+ ```
30
+
31
+ `wrangler dev` connects directly to the Postgres URL in `.dev.vars` (or
32
+ `localConnectionString` in `wrangler.jsonc`). Hyperdrive's pooling/caching
33
+ applies on Cloudflare's edge in production; locally you get a transparent
34
+ passthrough.
35
+
36
+ ## Module graph
37
+
38
+ ```sh
39
+ pnpm graph
40
+ # writes graph.html — open in any browser
41
+ ```
42
+
43
+ Generates a self-contained HTML page showing your modules, their dependency
44
+ edges, and a flat routes table. Useful for reviewing your app's structure or
45
+ embedding in PR descriptions. Regenerate after structural changes.
46
+
47
+ ## Type-safe clients with Hono RPC
48
+
49
+ `AppType` is exported from `src/app.ts`. Consume it from another package:
50
+
51
+ ```ts
52
+ import { hc } from 'hono/client';
53
+ import type { AppType } from '<your-api-package>';
54
+
55
+ const client = hc<AppType>('https://api.example.com');
56
+ const res = await client.posts.$post({ json: { title: 'Hi', body: 'Hello' } });
57
+ ```
58
+
59
+ ## Adding tests
60
+
61
+ If you add unit tests with synthetic services (e.g. test fixtures that
62
+ augment `@katajs/core`'s `Registry` with throwaway service keys), keep them
63
+ in their own tsconfig so test-only augmentations don't pollute editor
64
+ autocomplete in `src/`. TypeScript module augmentations are global per
65
+ program — splitting the program is the only fix.
66
+
67
+ Pattern:
68
+
69
+ ```jsonc
70
+ // tsconfig.json — for editor + `pnpm typecheck`
71
+ { "include": ["src/**/*"] }
72
+
73
+ // tsconfig.test.json — for `pnpm test:typecheck`
74
+ {
75
+ "extends": "./tsconfig.json",
76
+ "include": ["src/**/*", "test/**/*"]
77
+ }
78
+ ```
79
+
80
+ Run both in your `typecheck` script:
81
+ ```json
82
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json"
83
+ ```
84
+
85
+ ## Deploy
86
+
87
+ CI pushes to `main` deploy via the included workflow. Locally:
88
+
89
+ ```sh
90
+ pnpm deploy
91
+ ```
92
+
93
+ You'll need `CLOUDFLARE_API_TOKEN` in CI secrets and a real Hyperdrive
94
+ configuration before the first deploy. See
95
+ [Cloudflare Hyperdrive docs](https://developers.cloudflare.com/hyperdrive).
@@ -0,0 +1,3 @@
1
+ # Local connection string used by Hyperdrive in `wrangler dev`.
2
+ # Wrangler reads this env var if set; it overrides `localConnectionString` in wrangler.jsonc.
3
+ CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev"
@@ -0,0 +1,11 @@
1
+ node_modules/
2
+ dist/
3
+ .wrangler/
4
+ .dev.vars
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ *.log
9
+ .DS_Store
10
+ coverage/
11
+ *.tsbuildinfo
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ dialect: 'postgresql',
5
+ schema: './src/db/schema.ts',
6
+ out: './drizzle',
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL ?? 'postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev',
9
+ },
10
+ });
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "wrangler dev",
8
+ "deploy": "wrangler deploy",
9
+ "graph": "tsx scripts/graph.ts",
10
+ "db:generate": "drizzle-kit generate",
11
+ "db:migrate": "drizzle-kit migrate",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "@hono/zod-validator": "^0.4.1",
16
+ "@katajs/core": "^0.1.0",
17
+ "@katajs/drizzle": "^0.1.0",
18
+ "drizzle-orm": "^0.36.4",
19
+ "hono": "^4.6.10",
20
+ "pg": "^8.13.1",
21
+ "zod": "^3.23.8"
22
+ },
23
+ "devDependencies": {
24
+ "@cloudflare/workers-types": "^4.20241112.0",
25
+ "@types/node": "^22.9.0",
26
+ "@types/pg": "^8.11.10",
27
+ "drizzle-kit": "^0.28.1",
28
+ "tsx": "^4.19.2",
29
+ "typescript": "^5.6.3",
30
+ "wrangler": "^3.86.0"
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { inspectModules } from '@katajs/core';
5
+
6
+ import { modules } from './modules';
7
+
8
+ const here = fileURLToPath(new URL('.', import.meta.url));
9
+ const out = resolve(here, '..', 'graph.html');
10
+
11
+ const insp = inspectModules(modules);
12
+ writeFileSync(out, insp.html({ title: '{{PROJECT_NAME}} — module graph' }));
13
+
14
+ // eslint-disable-next-line no-console
15
+ console.log(`✓ wrote ${out}`);
16
+ // eslint-disable-next-line no-console
17
+ console.log(
18
+ ` ${insp.modules.length} modules • ${insp.edges.length} dependencies • ${insp.routes.length} routes`,
19
+ );
@@ -0,0 +1,7 @@
1
+ import { postsModule } from '../src/modules/posts/index';
2
+ // katajs:graph-imports
3
+
4
+ export const modules = [
5
+ postsModule,
6
+ // katajs:graph-modules
7
+ ];
@@ -0,0 +1,39 @@
1
+ import { createApp, type RequestVariables } from '@katajs/core';
2
+ import { drizzleAdapter } from '@katajs/drizzle';
3
+ import * as schema from './db/schema';
4
+
5
+ import { postsModule } from './modules/posts/index';
6
+ // katajs:module-imports
7
+
8
+ export type Bindings = {
9
+ HYPERDRIVE: Hyperdrive;
10
+ };
11
+
12
+ export type AppEnv = {
13
+ Bindings: Bindings;
14
+ Variables: RequestVariables;
15
+ };
16
+
17
+ const { app, queue } = createApp({
18
+ bindings: {} as Bindings,
19
+ db: drizzleAdapter({ schema }),
20
+ modules: [
21
+ postsModule,
22
+ // katajs:modules
23
+ ],
24
+ // Producer manifest. Each entry surfaces a typed wrapper at
25
+ // c.var.queues.<name>.send(body), validated against the schema.
26
+ queues: {
27
+ // katajs:queues
28
+ },
29
+ // Define your HTTP surface here. Add `.get()` / `.post()` for ad-hoc routes
30
+ // (health checks, webhooks) and `.route(prefix, module.routes)` per module.
31
+ routes: (base) =>
32
+ base
33
+ .route(postsModule.prefix, postsModule.routes),
34
+ // katajs:routes
35
+ });
36
+
37
+ export { queue };
38
+ export default app;
39
+ export type AppType = typeof app;
@@ -0,0 +1,11 @@
1
+ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
+
3
+ export const posts = pgTable('posts', {
4
+ id: uuid('id').primaryKey().defaultRandom(),
5
+ title: text('title').notNull(),
6
+ body: text('body').notNull(),
7
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
8
+ });
9
+
10
+ export type Post = typeof posts.$inferSelect;
11
+ export type NewPost = typeof posts.$inferInsert;
@@ -0,0 +1,6 @@
1
+ import app, { queue } from './app';
2
+
3
+ // Worker default export. `queue` is undefined when no module declares a
4
+ // `consumer:` field — Cloudflare Workers handles either case fine. When
5
+ // you add your first consumer, no change to this file is needed.
6
+ export default { fetch: app.fetch, queue };
@@ -0,0 +1,31 @@
1
+ import { defineModule } from '@katajs/core';
2
+ import { makePostRepository, type PostRepository } from './posts.repository';
3
+ import { makePostService, type PostsService } from './posts.service';
4
+ // katajs:module-service-imports
5
+ import { postsRoutes } from './posts.routes';
6
+
7
+ export const postsModule = defineModule({
8
+ name: 'posts',
9
+ provides: {
10
+ postRepository: (c): PostRepository => makePostRepository(c.db),
11
+ postService: (c): PostsService => makePostService(c),
12
+ // katajs:module-provides
13
+ },
14
+ requires: [] as const,
15
+ routes: postsRoutes,
16
+ prefix: '/posts',
17
+ // katajs:module-consumer
18
+ });
19
+
20
+ /**
21
+ * Services this module contributes to the container's `Registry`. Compose it
22
+ * in `app.ts` via `interface Registry extends PostsRegistry {}`. Adding a new
23
+ * module is one extra `extends` clause — no key-by-key bookkeeping.
24
+ */
25
+ export type PostsRegistry = {
26
+ postRepository: PostRepository;
27
+ postService: PostsService;
28
+ // katajs:module-registry
29
+ };
30
+
31
+ export type { PostRepository, PostsService };
@@ -0,0 +1,15 @@
1
+ import { AppError } from '@katajs/core';
2
+
3
+ export class PostNotFoundError extends AppError {
4
+ override readonly status = 404;
5
+ override readonly code = 'post_not_found';
6
+ override readonly publicMessage = 'Post not found';
7
+
8
+ constructor(public readonly postId: string) {
9
+ super(`Post ${postId} not found`);
10
+ }
11
+
12
+ override get publicPayload() {
13
+ return { postId: this.postId };
14
+ }
15
+ }
@@ -0,0 +1,22 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import type { DrizzleClientOrTx } from '@katajs/drizzle';
3
+ import { posts, type NewPost, type Post } from '../../db/schema';
4
+ import * as schema from '../../db/schema';
5
+
6
+ type Schema = typeof schema;
7
+
8
+ export function makePostRepository(dbOrTx: DrizzleClientOrTx<Schema>) {
9
+ return {
10
+ async findById(id: string): Promise<Post | undefined> {
11
+ const rows = await dbOrTx.select().from(posts).where(eq(posts.id, id)).limit(1);
12
+ return rows[0];
13
+ },
14
+ async insert(input: NewPost): Promise<Post> {
15
+ const [row] = await dbOrTx.insert(posts).values(input).returning();
16
+ if (!row) throw new Error('insert returned no row');
17
+ return row;
18
+ },
19
+ };
20
+ }
21
+
22
+ export type PostRepository = ReturnType<typeof makePostRepository>;
@@ -0,0 +1,20 @@
1
+ import { Hono } from 'hono';
2
+ import { validate } from '@katajs/core';
3
+ import type { AppEnv } from '../../app';
4
+ import { CreatePostSchema, PostIdParam } from './posts.schema';
5
+
6
+ export const postsRoutes = new Hono<AppEnv>()
7
+ .get('/:id', ...validate({ param: PostIdParam }), async (c) => {
8
+ const { id } = c.req.valid('param');
9
+ const service = c.var.resolve('postService');
10
+ const post = await service.getById(id);
11
+ return c.json({ post });
12
+ })
13
+ .post('/', ...validate({ body: CreatePostSchema }), async (c) => {
14
+ const input = c.req.valid('json');
15
+ const service = c.var.resolve('postService');
16
+ const post = await service.create(input);
17
+ return c.json({ post }, 201);
18
+ })
19
+ // katajs:module-routes
20
+ ;
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+
3
+ export const CreatePostSchema = z.object({
4
+ title: z.string().min(1).max(200),
5
+ body: z.string().min(1).max(10_000),
6
+ });
7
+ export type CreatePostInput = z.infer<typeof CreatePostSchema>;
8
+
9
+ export const PostIdParam = z.object({
10
+ id: z.string().uuid(),
11
+ });
12
+ export type PostIdParamInput = z.infer<typeof PostIdParam>;
@@ -0,0 +1,26 @@
1
+ import type { RequestContainer } from '@katajs/core';
2
+ import { PostNotFoundError } from './posts.errors';
3
+ import type { CreatePostInput } from './posts.schema';
4
+ import type { Post } from '../../db/schema';
5
+
6
+ export type PostsService = {
7
+ getById(id: string): Promise<Post>;
8
+ create(input: CreatePostInput): Promise<Post>;
9
+ };
10
+
11
+ export function makePostService(c: RequestContainer): PostsService {
12
+ return {
13
+ async getById(id) {
14
+ const repo = c.resolve('postRepository');
15
+ const post = await repo.findById(id);
16
+ if (!post) throw new PostNotFoundError(id);
17
+ return post;
18
+ },
19
+ async create(input) {
20
+ return c.withTransaction(async (tx) => {
21
+ const repo = tx.resolve('postRepository');
22
+ return repo.insert(input);
23
+ });
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Module augmentations for `@katajs/core`. Picked up automatically by
3
+ * tsconfig's `include` glob — no runtime import needed. Co-locate everything
4
+ * that teaches the framework about this app's concrete types here.
5
+ *
6
+ * The `katajs:registry-imports` and `katajs:registry` markers below are used
7
+ * by `katajs add module` to insert new module registry entries automatically.
8
+ * If you remove or relocate them, future `add module` runs will print the
9
+ * snippet for you to paste manually.
10
+ */
11
+ import type { DrizzleClient } from '@katajs/drizzle';
12
+ import type * as schema from './db/schema';
13
+ import type { Bindings } from './app';
14
+
15
+ import type { PostsRegistry } from './modules/posts/index';
16
+ // katajs:registry-imports
17
+
18
+ declare module '@katajs/core' {
19
+ interface AppDb extends DrizzleClient<typeof schema> {}
20
+ interface AppEnv extends Bindings {}
21
+ interface Registry
22
+ extends PostsRegistry
23
+ // katajs:registry
24
+ {}
25
+
26
+ // Typed producer queues. Each entry maps a queue name to its body type so
27
+ // c.var.queues.<name>.send(body) is fully typed.
28
+ interface QueuesRegistry {
29
+ // katajs:queues-registry
30
+ }
31
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2023"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "verbatimModuleSyntax": true,
13
+ "isolatedModules": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "declaration": true,
16
+ "declarationMap": true,
17
+ "sourceMap": true,
18
+ "noEmit": true,
19
+ "types": ["@cloudflare/workers-types", "node"]
20
+ },
21
+ "include": ["src/**/*", "scripts/**/*", "drizzle.config.ts"]
22
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "name": "{{PROJECT_NAME}}",
4
+ "main": "src/index.ts",
5
+ "compatibility_date": "{{TODAY_ISO}}",
6
+ "compatibility_flags": ["nodejs_compat"],
7
+ "observability": { "enabled": true },
8
+ "hyperdrive": [
9
+ {
10
+ "binding": "HYPERDRIVE",
11
+ "id": "<your-hyperdrive-id>",
12
+ "localConnectionString": "postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev"
13
+ }
14
+ ]
15
+
16
+ // Cloudflare Queues bindings. Uncomment + adjust when a module in this app
17
+ // declares a `consumer:` field (see docs/concepts/queues.md). Both halves
18
+ // are needed: producers (to send) and consumers (to receive).
19
+ //
20
+ // "queues": {
21
+ // "producers": [
22
+ // { "binding": "ORDER_QUEUE", "queue": "orders" },
23
+ // { "binding": "ORDER_DLQ", "queue": "orders-dlq" }
24
+ // ],
25
+ // "consumers": [
26
+ // {
27
+ // "queue": "orders",
28
+ // "max_batch_size": 100,
29
+ // "max_batch_timeout": 30,
30
+ // "max_retries": 3,
31
+ // "dead_letter_queue": "orders-dlq"
32
+ // }
33
+ // ]
34
+ // }
35
+ }
@@ -0,0 +1,53 @@
1
+ import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+
3
+ /**
4
+ * Better Auth tables. The exact column shape is dictated by Better Auth's
5
+ * Drizzle adapter — keep these in sync with `better-auth` releases. Run
6
+ * `pnpm db:generate` after changes.
7
+ */
8
+ export const user = pgTable('user', {
9
+ id: text('id').primaryKey(),
10
+ name: text('name').notNull(),
11
+ email: text('email').notNull().unique(),
12
+ emailVerified: boolean('email_verified').notNull().default(false),
13
+ image: text('image'),
14
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
15
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
16
+ });
17
+
18
+ export const session = pgTable('session', {
19
+ id: text('id').primaryKey(),
20
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
21
+ token: text('token').notNull().unique(),
22
+ ipAddress: text('ip_address'),
23
+ userAgent: text('user_agent'),
24
+ userId: text('user_id')
25
+ .notNull()
26
+ .references(() => user.id, { onDelete: 'cascade' }),
27
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
28
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
29
+ });
30
+
31
+ export const account = pgTable('account', {
32
+ id: text('id').primaryKey(),
33
+ accountId: text('account_id').notNull(),
34
+ providerId: text('provider_id').notNull(),
35
+ userId: text('user_id')
36
+ .notNull()
37
+ .references(() => user.id, { onDelete: 'cascade' }),
38
+ accessToken: text('access_token'),
39
+ refreshToken: text('refresh_token'),
40
+ idToken: text('id_token'),
41
+ password: text('password'),
42
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
43
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
44
+ });
45
+
46
+ export const verification = pgTable('verification', {
47
+ id: text('id').primaryKey(),
48
+ identifier: text('identifier').notNull(),
49
+ value: text('value').notNull(),
50
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
51
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
52
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
53
+ });
@@ -0,0 +1,30 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { drizzleAdapter as betterAuthDrizzleAdapter } from 'better-auth/adapters/drizzle';
3
+ import type { DrizzleClient } from '@katajs/drizzle';
4
+ import * as schema from '../../db/schema';
5
+
6
+ /**
7
+ * Build a Better Auth instance bound to the request-scoped Drizzle client.
8
+ *
9
+ * Note: Better Auth has its *own* `drizzleAdapter` (imported as
10
+ * `betterAuthDrizzleAdapter`) — distinct from `@katajs/drizzle`'s
11
+ * `drizzleAdapter`. The naming overlap is unavoidable.
12
+ */
13
+ export function makeAuth(db: DrizzleClient<typeof schema>, env: { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string }) {
14
+ return betterAuth({
15
+ database: betterAuthDrizzleAdapter(db, {
16
+ provider: 'pg',
17
+ schema: {
18
+ user: schema.user,
19
+ session: schema.session,
20
+ account: schema.account,
21
+ verification: schema.verification,
22
+ },
23
+ }),
24
+ secret: env.BETTER_AUTH_SECRET,
25
+ baseURL: env.BETTER_AUTH_URL,
26
+ emailAndPassword: { enabled: true },
27
+ });
28
+ }
29
+
30
+ export type AuthInstance = ReturnType<typeof makeAuth>;
@@ -0,0 +1,10 @@
1
+ import { AppError } from '@katajs/core';
2
+
3
+ export class UnauthorizedError extends AppError {
4
+ override readonly status = 401;
5
+ override readonly code = 'unauthorized';
6
+ override readonly publicMessage = 'Authentication required';
7
+ constructor() {
8
+ super('Unauthorized');
9
+ }
10
+ }
@@ -0,0 +1,37 @@
1
+ import { defineMiddleware } from '@katajs/core';
2
+ import type { AuthInstance } from './auth.config';
3
+ import { UnauthorizedError } from './auth.errors';
4
+
5
+ type SessionUser = {
6
+ id: string;
7
+ email: string;
8
+ name: string;
9
+ };
10
+
11
+ declare module 'hono' {
12
+ interface ContextVariableMap {
13
+ user: SessionUser;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Attach the current user (if any) to `c.var.user`. Routes that require auth
19
+ * should use `requireAuthMiddleware` instead.
20
+ */
21
+ export const attachUserMiddleware = (auth: AuthInstance) =>
22
+ defineMiddleware(async (c, next) => {
23
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
24
+ if (session?.user) {
25
+ c.set('user', session.user as SessionUser);
26
+ }
27
+ await next();
28
+ });
29
+
30
+ /** Require an authenticated user — throws `UnauthorizedError` if absent. */
31
+ export const requireAuthMiddleware = (auth: AuthInstance) =>
32
+ defineMiddleware(async (c, next) => {
33
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
34
+ if (!session?.user) throw new UnauthorizedError();
35
+ c.set('user', session.user as SessionUser);
36
+ await next();
37
+ });
@@ -0,0 +1,7 @@
1
+ import { Hono } from 'hono';
2
+ import type { AppEnv } from '../../app';
3
+
4
+ export const authRoutes = new Hono<AppEnv>().all('/*', async (c) => {
5
+ const auth = c.var.container.resolve('authInstance');
6
+ return auth.handler(c.req.raw);
7
+ });
@@ -0,0 +1,28 @@
1
+ import { defineModule } from '@katajs/core';
2
+ import * as schema from '../../db/schema';
3
+ import type { DrizzleClient } from '@katajs/drizzle';
4
+ import { makeAuth, type AuthInstance } from './auth.config';
5
+ import { authRoutes } from './auth.routes';
6
+
7
+ export const authModule = defineModule({
8
+ name: 'auth',
9
+ provides: {
10
+ authInstance: (c): AuthInstance =>
11
+ makeAuth(
12
+ c.db as DrizzleClient<typeof schema>,
13
+ c.env as { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string },
14
+ ),
15
+ },
16
+ requires: [] as const,
17
+ routes: authRoutes,
18
+ prefix: '/auth',
19
+ });
20
+
21
+ /** Services this module contributes to the container's `Registry`. */
22
+ export type AuthRegistry = {
23
+ authInstance: AuthInstance;
24
+ };
25
+
26
+ export type { AuthInstance } from './auth.config';
27
+ export { UnauthorizedError } from './auth.errors';
28
+ export { attachUserMiddleware, requireAuthMiddleware } from './auth.middleware';
@@ -0,0 +1,10 @@
1
+ import { AppError } from '@katajs/core';
2
+
3
+ export class UnauthorizedError extends AppError {
4
+ override readonly status = 401;
5
+ override readonly code = 'unauthorized';
6
+ override readonly publicMessage = 'Authentication required';
7
+ constructor() {
8
+ super('Unauthorized');
9
+ }
10
+ }