@podosoft/podokit 0.5.1 → 0.7.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 (98) hide show
  1. package/README.md +6 -0
  2. package/dist/add.d.ts +24 -2
  3. package/dist/add.js +77 -20
  4. package/dist/assemble.js +4 -1
  5. package/dist/create.d.ts +2 -0
  6. package/dist/create.js +6 -0
  7. package/dist/eject.js +9 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +7 -2
  10. package/dist/lib.d.ts +14 -0
  11. package/dist/lib.js +34 -0
  12. package/dist/lockfile.d.ts +7 -2
  13. package/dist/lockfile.js +37 -2
  14. package/dist/templates/base/AGENTS.md +30 -0
  15. package/dist/templates/base/CLAUDE.md +6 -0
  16. package/dist/templates/fullstack-nest-svelte/AGENTS.md +79 -0
  17. package/dist/templates/fullstack-nest-svelte/CLAUDE.md +6 -0
  18. package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +25 -0
  19. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +4 -2
  20. package/dist/templates/fullstack-nest-svelte/apps/api/src/app.extensions.ts +21 -0
  21. package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +3 -0
  22. package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +2 -0
  23. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/index.ts +1 -3
  24. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +1 -1
  25. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +5 -25
  26. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +1 -4
  27. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs.svelte +1 -1
  28. package/dist/templates/fullstack-nest-svelte/apps/web/static/favicon.svg +13 -0
  29. package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +12 -0
  30. package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +150 -0
  31. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-module/SKILL.md +27 -0
  32. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-nest-endpoint/SKILL.md +23 -0
  33. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-sveltekit-route/SKILL.md +23 -0
  34. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-update/SKILL.md +26 -0
  35. package/dist/templates/fullstack-nest-svelte/dot-cursor/rules/podokit.mdc +11 -0
  36. package/dist/templates/fullstack-nest-svelte/dot-devcontainer/devcontainer.json +20 -0
  37. package/dist/templates/fullstack-nest-svelte/dot-dockerignore +8 -0
  38. package/dist/templates/fullstack-nest-svelte/dot-env.docker +26 -0
  39. package/dist/templates/fullstack-nest-svelte/dot-github/copilot-instructions.md +6 -0
  40. package/dist/templates/fullstack-nest-svelte/dot-mcp.json +8 -0
  41. package/dist/templates/fullstack-nest-svelte/infra/traefik/dynamic.yml +13 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +87 -0
  43. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.module.ts +14 -0
  44. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +84 -0
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +2 -0
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +12 -1
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +37 -0
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +25 -11
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +32 -0
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +16 -3
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +52 -2
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +50 -0
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +38 -13
  54. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +203 -0
  55. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/+layout.svelte +7 -3
  56. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +8 -3
  57. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +34 -4
  58. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +41 -0
  59. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +2 -0
  60. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/maintenance/+page.svelte +25 -0
  61. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +43 -0
  62. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +81 -0
  63. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +42 -1
  64. package/dist/templates/modules/admin-dashboard/module.manifest.json +22 -4
  65. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +27 -0
  66. package/dist/templates/modules/auth/module.manifest.json +9 -8
  67. package/dist/templates/modules/bullmq/module.manifest.json +1 -1
  68. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.controller.ts +3 -0
  69. package/dist/templates/modules/mailer/files/apps/api/src/mail/db.ts +13 -0
  70. package/dist/templates/modules/{auth → mailer}/files/apps/api/src/mail/mailer.ts +41 -7
  71. package/dist/templates/modules/mailer/module.manifest.json +38 -0
  72. package/dist/templates/todo/AGENTS.md +80 -0
  73. package/dist/templates/todo/CLAUDE.md +6 -0
  74. package/dist/templates/todo/Dockerfile.dev +25 -0
  75. package/dist/templates/todo/apps/api/package.json +4 -2
  76. package/dist/templates/todo/apps/api/src/app.extensions.ts +21 -0
  77. package/dist/templates/todo/apps/api/src/app.module.ts +3 -0
  78. package/dist/templates/todo/apps/web/src/app.html +2 -0
  79. package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/index.ts +1 -3
  80. package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +1 -1
  81. package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +5 -25
  82. package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +1 -4
  83. package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs.svelte +1 -1
  84. package/dist/templates/todo/apps/web/static/favicon.svg +13 -0
  85. package/dist/templates/todo/apps/web/vite.config.ts +12 -0
  86. package/dist/templates/todo/compose.dev.yaml +150 -0
  87. package/dist/templates/todo/dot-claude/skills/podokit-add-module/SKILL.md +27 -0
  88. package/dist/templates/todo/dot-claude/skills/podokit-nest-endpoint/SKILL.md +23 -0
  89. package/dist/templates/todo/dot-claude/skills/podokit-sveltekit-route/SKILL.md +23 -0
  90. package/dist/templates/todo/dot-claude/skills/podokit-update/SKILL.md +26 -0
  91. package/dist/templates/todo/dot-cursor/rules/podokit.mdc +11 -0
  92. package/dist/templates/todo/dot-devcontainer/devcontainer.json +20 -0
  93. package/dist/templates/todo/dot-dockerignore +8 -0
  94. package/dist/templates/todo/dot-env.docker +26 -0
  95. package/dist/templates/todo/dot-github/copilot-instructions.md +6 -0
  96. package/dist/templates/todo/dot-mcp.json +8 -0
  97. package/dist/templates/todo/infra/traefik/dynamic.yml +13 -0
  98. package/package.json +9 -1
@@ -45,7 +45,34 @@ export function createFeatureGate(pool: Pool) {
45
45
  return cache;
46
46
  }
47
47
 
48
+ // Registration can be closed from the admin Settings ("Allow sign-up"). It is a
49
+ // site setting (`site.allowSignup`), so the row is absent until an admin turns
50
+ // it off — a missing/any-non-"false" value means open, matching the shipped
51
+ // default. Cached the same way as the feature flags.
52
+ let signupOpen = true;
53
+ let signupFetchedAt = 0;
54
+ async function isSignupOpen(): Promise<boolean> {
55
+ if (Date.now() - signupFetchedAt < CACHE_TTL_MS) return signupOpen;
56
+ try {
57
+ const res = await pool.query<{ value: string }>(
58
+ 'SELECT "value" FROM "app_setting" WHERE "key" = $1',
59
+ ["site.allowSignup"],
60
+ );
61
+ signupOpen = res.rows[0]?.value !== "false";
62
+ } catch {
63
+ signupOpen = true;
64
+ }
65
+ signupFetchedAt = Date.now();
66
+ return signupOpen;
67
+ }
68
+
48
69
  return createAuthMiddleware(async (ctx) => {
70
+ // Block email/password registration when sign-up is closed. Social/invite
71
+ // flows are unaffected; admins create users from the dashboard.
72
+ if (ctx.path === "/sign-up/email" || ctx.path.startsWith("/sign-up/")) {
73
+ if (!(await isSignupOpen())) throw new APIError("FORBIDDEN", { message: "Sign-up is disabled." });
74
+ return;
75
+ }
49
76
  const feature = FEATURE_PATHS.find((f) =>
50
77
  f.prefixes.some((p) => ctx.path === p || ctx.path.startsWith(p)),
51
78
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "auth",
3
3
  "description": "Full authentication (better-auth): email/password, sessions, and \u2014 via config \u2014 OAuth and 2FA. Installs a global guard so the API is secure by default; health and /api/auth stay public.",
4
- "requires": [],
4
+ "requires": ["mailer"],
5
5
  "targetApp": "api",
6
6
  "dependencies": {
7
7
  "@podosoft/podokit-auth": "^0.1.0",
@@ -9,14 +9,12 @@
9
9
  "better-auth": "^1.6.23",
10
10
  "@thallesp/nestjs-better-auth": "^2.7.0",
11
11
  "pg": "^8.12.0",
12
- "nodemailer": "^7.0.9",
13
12
  "@better-auth/api-key": "^1.6.23",
14
13
  "@better-auth/passkey": "^1.6.23",
15
14
  "@better-auth/oauth-provider": "^1.6.23"
16
15
  },
17
16
  "devDependencies": {
18
- "@types/pg": "^8.11.10",
19
- "@types/nodemailer": "^7.0.4"
17
+ "@types/pg": "^8.11.10"
20
18
  },
21
19
  "env": [
22
20
  "# \u2500\u2500 Auth (better-auth) \u2014 the only required secrets \u2500\u2500",
@@ -32,10 +30,7 @@
32
30
  "# override (e.g. for secrets-manager injection in production); the DB takes",
33
31
  "# precedence when set.",
34
32
  "# --- Optional overrides / local development ---",
35
- "# Email (Mailpit in local dev: docker compose --profile dev up \u2192 http://localhost:8025)",
36
- "# SMTP_HOST=localhost",
37
- "# SMTP_PORT=1025",
38
- "# MAIL_FROM=PodoKit <no-reply@example.com>",
33
+ "# Email/SMTP is provided by the mailer module (see its .env block).",
39
34
  "# Phone-number SMS webhook (dev: SMS sink on port 8095)",
40
35
  "# SMS_WEBHOOK_URL=http://localhost:8095/sms",
41
36
  "# AUTH_HIBP=false",
@@ -91,6 +86,12 @@
91
86
  "file": "apps/api/src/app.module.ts",
92
87
  "marker": "// podokit:end:providers",
93
88
  "text": "{ provide: APP_GUARD, useClass: AuthGuard },"
89
+ },
90
+ {
91
+ "file": "AGENTS.md",
92
+ "marker": "<!-- podokit:end:agents-modules -->",
93
+ "optional": true,
94
+ "text": "### auth (better-auth)\n- The API is **secure by default**: every route needs a session except `/health` and `/api/auth/*`. Opt a route out with `@Public()`; read the user with `@Session()`.\n- Configure OAuth providers, SMTP, and the server toggles on the admin **Settings** page (DB-backed, applied live \u2014 not via env). Only `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are required env vars.\n- Create the auth tables: `npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts`.\n"
94
95
  }
95
96
  ],
96
97
  "instructions": [
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start:worker": "node dist/main-worker",
12
- "dev:worker": "nest start --watch --entryFile main-worker"
12
+ "dev:worker": "nest start --watch --builder swc --entryFile main-worker"
13
13
  },
14
14
  "env": [
15
15
  "# bullmq uses REDIS_HOST / REDIS_PORT (already defined above)"
@@ -1,3 +1,6 @@
1
+ // The API tsconfig restricts `types` to ["node"], which suppresses @types/multer's
2
+ // global augmentation of Express.Multer.File. Load it explicitly so the type resolves.
3
+ /// <reference types="multer" />
1
4
  import { BadRequestException, Controller, Get, Param, Post, UploadedFile, UseInterceptors } from "@nestjs/common";
2
5
  import { FileInterceptor } from "@nestjs/platform-express";
3
6
  import { ApiConsumes, ApiTags } from "@nestjs/swagger";
@@ -0,0 +1,13 @@
1
+ import { Pool } from "pg";
2
+
3
+ // The mailer's own pg pool, in its own module so the mailer stays independent of
4
+ // any other module. Used only to read the optional admin-configured SMTP settings
5
+ // from `auth_config` when the auth module is present; with no such table the read
6
+ // fails softly and the mailer falls back to SMTP_* env (see mailer.ts).
7
+ export const pool = new Pool({
8
+ host: process.env.POSTGRES_HOST ?? "localhost",
9
+ port: Number(process.env.POSTGRES_PORT ?? 5432),
10
+ user: process.env.POSTGRES_USER ?? "podokit",
11
+ password: process.env.POSTGRES_PASSWORD ?? "podokit",
12
+ database: process.env.POSTGRES_DB ?? "podokit",
13
+ });
@@ -1,14 +1,40 @@
1
1
  import nodemailer, { type Transporter } from "nodemailer";
2
- import { pool } from "../auth/db";
2
+ import { pool } from "./db";
3
3
  import { createConfigStore, type SmtpConfig } from "@podosoft/podokit-auth";
4
4
 
5
- // SMTP is admin-configurable in the DB (auth_config), falling back to SMTP_* env,
6
- // and applied live: the transport is rebuilt when the config changes (short TTL),
7
- // so no restart is needed. With neither DB nor env config, fall back to a transport
8
- // that just logs the message so nothing crashes for lack of mail config.
5
+ // Reusable email sending. SMTP is resolved in priority order, applied live (short
6
+ // TTL) so no restart is needed:
7
+ // 1. admin-configured DB settings (auth_config) — only when the auth module is
8
+ // installed; read via the config store, which decrypts the SMTP password;
9
+ // 2. SMTP_* env (fallback / production secrets-manager injection);
10
+ // 3. none → a JSON transport that logs the message so dev links are grabbable.
11
+ // The whole transport can also be overridden from app code (setMailTransport),
12
+ // e.g. from apps/api/src/app.extensions.ts, to plug in a provider SDK.
9
13
  const store = createConfigStore(pool);
10
14
  const TTL_MS = 3_000;
11
15
 
16
+ function envSmtp(): SmtpConfig | null {
17
+ if (!process.env.SMTP_HOST) return null;
18
+ return {
19
+ host: process.env.SMTP_HOST,
20
+ port: Number(process.env.SMTP_PORT ?? 1025),
21
+ secure: process.env.SMTP_SECURE === "true",
22
+ user: process.env.SMTP_USER,
23
+ pass: process.env.SMTP_PASS,
24
+ from: process.env.MAIL_FROM,
25
+ };
26
+ }
27
+
28
+ async function resolveSmtp(): Promise<SmtpConfig | null> {
29
+ try {
30
+ // Reads auth_config and falls back to env internally when auth is installed.
31
+ return await store.smtpConfig();
32
+ } catch {
33
+ // No auth_config table (auth not installed) — use env directly.
34
+ return envSmtp();
35
+ }
36
+ }
37
+
12
38
  function buildTransport(smtp: SmtpConfig | null): Transporter {
13
39
  if (!smtp) return nodemailer.createTransport({ jsonTransport: true });
14
40
  return nodemailer.createTransport({
@@ -24,12 +50,20 @@ let transportFrom = process.env.MAIL_FROM ?? "PodoKit <no-reply@example.com>";
24
50
  let hasSmtp = false;
25
51
  let checkedAt = 0;
26
52
  let signature = "";
53
+ let override: Transporter | null = null;
54
+
55
+ /** Override the transport from app code (e.g. a provider SDK). Pass null to
56
+ * restore the config-driven transport. Set this from app.extensions.ts. */
57
+ export function setMailTransport(custom: Transporter | null): void {
58
+ override = custom;
59
+ }
27
60
 
28
61
  async function getTransport(): Promise<Transporter> {
62
+ if (override) return override;
29
63
  if (checkedAt !== 0 && Date.now() - checkedAt < TTL_MS) return transport;
30
64
  checkedAt = Date.now();
31
65
  try {
32
- const smtp = await store.smtpConfig();
66
+ const smtp = await resolveSmtp();
33
67
  const next = JSON.stringify(smtp ?? null);
34
68
  if (next !== signature) {
35
69
  transport = buildTransport(smtp);
@@ -48,7 +82,7 @@ export type Mail = { to: string; subject: string; text: string; html?: string };
48
82
  export async function sendMail(mail: Mail): Promise<void> {
49
83
  const t = await getTransport();
50
84
  const info = await t.sendMail({ from: transportFrom, ...mail });
51
- if (!hasSmtp) {
85
+ if (!override && !hasSmtp) {
52
86
  // JSON transport: surface the message so the link is grabbable from logs.
53
87
  console.log(`[mailer] no SMTP configured; message not delivered:\n${info.message as string}`);
54
88
  }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "mailer",
3
+ "description": "Reusable email sending (nodemailer). Resolves SMTP from the admin-configured DB settings when auth is installed, else SMTP_* env, else logs to the console. Used by auth and contact-form; the transport is overridable from app.extensions.ts.",
4
+ "requires": [],
5
+ "targetApp": "api",
6
+ "dependencies": {
7
+ "nodemailer": "^7.0.9",
8
+ "@podosoft/podokit-auth": "^0.1.0",
9
+ "pg": "^8.12.0"
10
+ },
11
+ "devDependencies": {
12
+ "@types/nodemailer": "^7.0.4",
13
+ "@types/pg": "^8.11.10"
14
+ },
15
+ "env": [
16
+ "# ── Mailer (SMTP) — optional ──",
17
+ "# With auth installed, configure SMTP on the admin Settings page (stored",
18
+ "# encrypted in the DB, applied live). These env vars are a fallback/override;",
19
+ "# with no SMTP configured at all the mailer logs messages to the console.",
20
+ "# Local dev uses Mailpit: docker compose --profile dev up → http://localhost:8025",
21
+ "# SMTP_HOST=localhost",
22
+ "# SMTP_PORT=1025",
23
+ "# MAIL_FROM=PodoKit <no-reply@example.com>"
24
+ ],
25
+ "inject": [
26
+ {
27
+ "file": "AGENTS.md",
28
+ "marker": "<!-- podokit:end:agents-modules -->",
29
+ "optional": true,
30
+ "text": "### mailer\n- Send email with `import { sendMail, actionEmail } from \"../mail/mailer\"` (path relative to your file). `sendMail({ to, subject, text, html? })`.\n- SMTP comes from the admin Settings page (when auth is installed) or `SMTP_*` env; with neither, messages are logged to the console (dev links are grabbable from logs).\n- To use a provider SDK instead of SMTP, call `setMailTransport(...)` from `apps/api/src/app.extensions.ts`.\n"
31
+ }
32
+ ],
33
+ "instructions": [
34
+ "Send mail from anywhere in the API: import { sendMail } from \"../mail/mailer\" then await sendMail({ to, subject, text }).",
35
+ "SMTP is optional: with auth installed, configure it on the admin Settings page (DB, applied live); otherwise set SMTP_HOST/SMTP_PORT/MAIL_FROM in .env. With none set, messages are logged to the console.",
36
+ "To send through a provider SDK, override the transport in apps/api/src/app.extensions.ts: import { setMailTransport } from \"./mail/mailer\"; setMailTransport(myTransport)."
37
+ ]
38
+ }
@@ -0,0 +1,80 @@
1
+ # AGENTS.md — {{projectName}}
2
+
3
+ Guidance for AI coding agents (Claude Code, Codex, Cursor, Copilot, …) working in
4
+ this repository. It was generated by [PodoKit](https://github.com/podosoft-dev/podokit).
5
+ Follow it so your changes match the project's conventions.
6
+
7
+ ## Project overview
8
+
9
+ A full-stack TypeScript monorepo (npm workspaces):
10
+
11
+ - `apps/api` — **NestJS** backend (`{{projectName}}-api`), TypeORM + PostgreSQL, Swagger at `/api-docs`.
12
+ - `apps/web` — **SvelteKit** frontend (`{{projectName}}-web`), Tailwind v4 + shadcn-svelte + typesafe-i18n.
13
+ - `infra/` — Docker Compose (PostgreSQL/Redis) and example k3s manifests.
14
+ - `.podokit/` — PodoKit's generation lockfile (see "PodoKit tooling" below). Do not edit by hand.
15
+ - **Todo example**: `apps/api/src/todos/` (controller/service/entity/DTO + migration) and the todo UI in `apps/web/src/routes/` show the end-to-end patterns to follow.
16
+
17
+ ## Setup & commands
18
+
19
+ ```bash
20
+ npm install
21
+ cp .env.example .env
22
+ docker compose -f infra/docker/docker-compose.yml up -d # PostgreSQL (+ Redis)
23
+ npm run dev # api on http://localhost:5002, web on http://localhost:5001
24
+ ```
25
+
26
+ - **Ports: web = 5001, api = 5002.** The web app reaches the API only through its
27
+ SvelteKit server proxy (`/api/*`), never with a direct browser `fetch`.
28
+ - Build: `npm run build` · Type-check/lint: `npm run lint` · Unit tests: `npm test`
29
+ - Per workspace: `npm run build -w {{projectName}}-api` / `-w {{projectName}}-web`.
30
+ - DB migrations (TypeORM): `npm run migration:run -w {{projectName}}-api`.
31
+ - e2e (Playwright, ships in `tests/`): `npm run test:e2e`.
32
+
33
+ **Always** verify a change by building the affected workspace (and `svelte-check`
34
+ for web) before considering it done. UI changes need a Playwright test; API
35
+ changes need an e2e/unit test.
36
+
37
+ ## Code style
38
+
39
+ ### TypeScript (both apps)
40
+ - `strict` mode. **No `any`** — use `unknown` + narrowing. **No `@ts-ignore`/`@ts-expect-error`** and no `eslint-disable`.
41
+ - Explicit function return types. DTOs use `class-validator` decorators.
42
+ - Keep `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noUncheckedIndexedAccess`.
43
+
44
+ ### Svelte 5 (web)
45
+ - **Runes only**: `$state()`, `$derived()`, `$effect()`, `$props()`. **Never** use `$:` (deprecated).
46
+ - Reactive stores go in `.svelte.ts` files; don't destructure a getter-exported rune (it loses reactivity) — wrap in `$derived`.
47
+
48
+ ### UI — Tailwind + shadcn-svelte (web)
49
+ - Prefer **shadcn-svelte components** over raw HTML (`<button>`, `<input>`, `<select>`, …).
50
+ - **Do not edit `src/lib/components/ui/**`** (the shadcn primitives). Customize in a separate wrapper.
51
+ - Tailwind v4 (CSS-first, no `tailwind.config`). Use semantic classes: `bg-background`, `text-foreground`, `border-border`. Dark mode via `mode-watcher`. Responsive (`sm:`/`md:`/`lg:`).
52
+
53
+ ### i18n & errors
54
+ - **Every user-facing string goes through i18n** (`src/lib/i18n/messages.ts`) — no hardcoded text. Add keys to **all** locales at once.
55
+ - The backend returns a stable **error envelope**: `{ success: false, error: { code, message, statusCode, path, timestamp, details? } }`. Throw `AppException("SOME_CODE", ...)` on the backend; **branch on `error.code`, not the message,** on the frontend.
56
+ - Logging is structured; no emojis/icons. Start messages with an action verb.
57
+
58
+ ## PodoKit tooling & file ownership
59
+
60
+ This project is managed by the `podo` CLI. Files have an ownership tier recorded
61
+ in `.podokit/files.lock` — respect it:
62
+
63
+ - **owned** (your code: `apps/web/src/routes/**`, your components/services, `apps/api/src/app.extensions.ts`, and this `AGENTS.md`) — edit freely. To add or override Nest providers/modules, use `app.extensions.ts` (spread into `AppModule` after the module-wired providers, so a same-token override wins) rather than editing `app.module.ts`.
64
+ - **assembled** (e.g. `apps/api/src/app.module.ts`, `auth.ts`) — contain `// podokit:begin:… / …:end` fenced regions that the toolkit recomputes. Edit outside the fences only; **never hand-edit inside the fences**.
65
+ - **managed** (bootstrap/config like `main.ts`) — the toolkit may update these; avoid gratuitous edits.
66
+
67
+ Useful commands:
68
+
69
+ - `podo add <module>` — add a feature (auth, admin-dashboard, redis, bullmq, …). It wires itself into the app; run `podo add` with no argument to list modules.
70
+ - `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
71
+ - `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
72
+
73
+ ## Commit conventions
74
+
75
+ Conventional Commits (`feat|fix|docs|refactor|test|chore`), imperative mood,
76
+ ≤50-char subject, no trailing period, no emojis. One logical change per commit.
77
+
78
+ <!-- podokit:begin:agents-modules -->
79
+ <!-- Module-specific guidance is injected here by `podo add`. -->
80
+ <!-- podokit:end:agents-modules -->
@@ -0,0 +1,6 @@
1
+ # CLAUDE.md
2
+
3
+ Agent guidance for this project lives in [AGENTS.md](./AGENTS.md). It is imported
4
+ below so Claude Code loads the same rules the other AI tools read.
5
+
6
+ @AGENTS.md
@@ -0,0 +1,25 @@
1
+ # Development image for the PodoKit monorepo (web + api).
2
+ # Dependencies and source are baked in; `docker compose watch` syncs your
3
+ # edits into the running container so the native file watchers fire.
4
+ FROM node:22-bookworm-slim
5
+
6
+ # git for the toolchain (editors, AI agents); build tools for native modules.
7
+ RUN apt-get update \
8
+ && apt-get install -y --no-install-recommends git python3 make g++ ca-certificates \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ WORKDIR /app
12
+
13
+ # Install workspace dependencies (cached unless a package.json changes).
14
+ # NB: the host package-lock.json is intentionally NOT copied — a lockfile
15
+ # generated on macOS/Windows prunes platform-specific optional deps and can drop
16
+ # packages (e.g. @swc/cli) on linux. Resolving fresh in-container avoids that.
17
+ COPY package.json ./
18
+ COPY apps/api/package.json ./apps/api/package.json
19
+ COPY apps/web/package.json ./apps/web/package.json
20
+ RUN npm install
21
+
22
+ # Baked source (kept in sync during development by `docker compose watch`).
23
+ COPY . .
24
+
25
+ EXPOSE 5001 5002
@@ -3,7 +3,7 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "nest start --watch",
6
+ "dev": "nest start --watch --builder swc",
7
7
  "build": "nest build",
8
8
  "start": "node dist/main",
9
9
  "lint": "tsc -p tsconfig.json --noEmit",
@@ -40,7 +40,9 @@
40
40
  "supertest": "^7.0.0",
41
41
  "ts-jest": "^29.2.5",
42
42
  "ts-node": "^10.9.2",
43
- "typescript": "^6.0.3"
43
+ "typescript": "^6.0.3",
44
+ "@swc/core": "^1.7.0",
45
+ "@swc/cli": "^0.4.0"
44
46
  },
45
47
  "jest": {
46
48
  "moduleFileExtensions": [
@@ -0,0 +1,21 @@
1
+ import type { DynamicModule, Provider, Type } from "@nestjs/common";
2
+
3
+ // Owned extension slot for the Nest app. This file is yours — PodoKit never
4
+ // writes to it, so `podo update` leaves it alone. Use it to add your own modules
5
+ // or providers, or to override a PodoKit-provided provider without editing
6
+ // managed code. Because these are spread in *after* the module-wired providers,
7
+ // an override here takes precedence.
8
+ //
9
+ // Example — override a provider with your own implementation:
10
+ // import { SomeService } from "./some/some.service";
11
+ // import { MyService } from "./my-service";
12
+ // export const extensionProviders: Provider[] = [
13
+ // { provide: SomeService, useClass: MyService },
14
+ // ];
15
+
16
+ /** Extra modules to import into AppModule (or DynamicModule overrides). */
17
+ export const extensionImports: (Type | DynamicModule)[] = [];
18
+
19
+ /** Extra or overriding providers for AppModule. Same-token providers listed here
20
+ * win over the ones PodoKit modules registered. */
21
+ export const extensionProviders: Provider[] = [];
@@ -5,6 +5,7 @@ import { validateEnv } from "./config/env.validation";
5
5
  import { dataSourceOptions } from "./database/data-source";
6
6
  import { HealthModule } from "./health/health.module";
7
7
  import { TodosModule } from "./todos/todos.module";
8
+ import { extensionImports, extensionProviders } from "./app.extensions";
8
9
  // podokit:begin:imports
9
10
  // podokit:end:imports
10
11
 
@@ -16,10 +17,12 @@ import { TodosModule } from "./todos/todos.module";
16
17
  TodosModule,
17
18
  // podokit:begin:module-imports
18
19
  // podokit:end:module-imports
20
+ ...extensionImports,
19
21
  ],
20
22
  providers: [
21
23
  // podokit:begin:providers
22
24
  // podokit:end:providers
25
+ ...extensionProviders,
23
26
  ],
24
27
  })
25
28
  export class AppModule {}
@@ -3,6 +3,8 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{projectName}}</title>
7
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
6
8
  %sveltekit.head%
7
9
  </head>
8
10
  <body data-sveltekit-preload-data="hover" class="h-full">
@@ -1,6 +1,6 @@
1
1
  import Root from "./tabs.svelte";
2
2
  import Content from "./tabs-content.svelte";
3
- import List, { tabsListVariants, type TabsListVariant } from "./tabs-list.svelte";
3
+ import List from "./tabs-list.svelte";
4
4
  import Trigger from "./tabs-trigger.svelte";
5
5
 
6
6
  export {
@@ -8,8 +8,6 @@ export {
8
8
  Content,
9
9
  List,
10
10
  Trigger,
11
- tabsListVariants,
12
- type TabsListVariant,
13
11
  //
14
12
  Root as Tabs,
15
13
  Content as TabsContent,
@@ -12,6 +12,6 @@
12
12
  <TabsPrimitive.Content
13
13
  bind:ref
14
14
  data-slot="tabs-content"
15
- class={cn("text-sm flex-1 outline-none", className)}
15
+ class={cn("flex-1 outline-none", className)}
16
16
  {...restProps}
17
17
  />
@@ -1,40 +1,20 @@
1
- <script lang="ts" module>
2
- import { tv, type VariantProps } from "tailwind-variants";
3
-
4
- export const tabsListVariants = tv({
5
- base: "rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
6
- variants: {
7
- variant: {
8
- default: "cn-tabs-list-variant-default bg-muted",
9
- line: "cn-tabs-list-variant-line gap-1 bg-transparent",
10
- },
11
- },
12
- defaultVariants: {
13
- variant: "default",
14
- },
15
- });
16
-
17
- export type TabsListVariant = VariantProps<typeof tabsListVariants>["variant"];
18
- </script>
19
-
20
1
  <script lang="ts">
21
2
  import { Tabs as TabsPrimitive } from "bits-ui";
22
3
  import { cn } from "$lib/utils.js";
23
4
 
24
5
  let {
25
6
  ref = $bindable(null),
26
- variant = "default",
27
7
  class: className,
28
8
  ...restProps
29
- }: TabsPrimitive.ListProps & {
30
- variant?: TabsListVariant;
31
- } = $props();
9
+ }: TabsPrimitive.ListProps = $props();
32
10
  </script>
33
11
 
34
12
  <TabsPrimitive.List
35
13
  bind:ref
36
14
  data-slot="tabs-list"
37
- data-variant={variant}
38
- class={cn(tabsListVariants({ variant }), className)}
15
+ class={cn(
16
+ "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
17
+ className
18
+ )}
39
19
  {...restProps}
40
20
  />
@@ -13,10 +13,7 @@
13
13
  bind:ref
14
14
  data-slot="tabs-trigger"
15
15
  class={cn(
16
- "gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
17
- "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
18
- "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground",
19
- "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
16
+ "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
20
17
  className
21
18
  )}
22
19
  {...restProps}
@@ -14,6 +14,6 @@
14
14
  bind:ref
15
15
  bind:value
16
16
  data-slot="tabs"
17
- class={cn("gap-2 group/tabs flex data-[orientation=horizontal]:flex-col", className)}
17
+ class={cn("flex flex-col gap-2", className)}
18
18
  {...restProps}
19
19
  />
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="PodoKit">
2
+ <defs>
3
+ <linearGradient id="pk" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#8b5cf6"/>
5
+ <stop offset="1" stop-color="#4f46e5"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="15" fill="url(#pk)"/>
9
+ <!-- layered stack: full-stack toolkit -->
10
+ <path d="M32 14 50 25 32 36 14 25Z" fill="#fff" fill-opacity="0.96"/>
11
+ <path d="M16 31 32 40.5 48 31" fill="none" stroke="#fff" stroke-opacity="0.78" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <path d="M16 38 32 47.5 48 38" fill="none" stroke="#fff" stroke-opacity="0.5" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/>
13
+ </svg>
@@ -2,6 +2,18 @@ import { sveltekit } from "@sveltejs/kit/vite";
2
2
  import tailwindcss from "@tailwindcss/vite";
3
3
  import { defineConfig } from "vite";
4
4
 
5
+ // Inside the dev container (compose.dev.yaml sets VITE_DOCKER=1): bind to all
6
+ // interfaces, allow the Traefik hostname, and route HMR through Traefik on :80.
7
+ const inDocker = process.env.VITE_DOCKER === "1";
8
+
5
9
  export default defineConfig({
6
10
  plugins: [tailwindcss(), sveltekit()],
11
+ server: inDocker
12
+ ? {
13
+ host: true,
14
+ port: 5001,
15
+ allowedHosts: true,
16
+ hmr: { clientPort: 80 },
17
+ }
18
+ : undefined,
7
19
  });