@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
@@ -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 — swap the mailer transport for your own implementation:
10
+ // import { Mailer } from "./mailer/mailer";
11
+ // import { MyMailer } from "./my-mailer";
12
+ // export const extensionProviders: Provider[] = [
13
+ // { provide: Mailer, useClass: MyMailer },
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[] = [];
@@ -4,6 +4,7 @@ import { TypeOrmModule } from "@nestjs/typeorm";
4
4
  import { validateEnv } from "./config/env.validation";
5
5
  import { dataSourceOptions } from "./database/data-source";
6
6
  import { HealthModule } from "./health/health.module";
7
+ import { extensionImports, extensionProviders } from "./app.extensions";
7
8
  // podokit:begin:imports
8
9
  // podokit:end:imports
9
10
 
@@ -14,10 +15,12 @@ import { HealthModule } from "./health/health.module";
14
15
  HealthModule,
15
16
  // podokit:begin:module-imports
16
17
  // podokit:end:module-imports
18
+ ...extensionImports,
17
19
  ],
18
20
  providers: [
19
21
  // podokit:begin:providers
20
22
  // podokit:end:providers
23
+ ...extensionProviders,
21
24
  ],
22
25
  })
23
26
  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
  });
@@ -0,0 +1,150 @@
1
+ # Containerized development environment.
2
+ #
3
+ # docker compose -f compose.dev.yaml watch # core stack
4
+ # docker compose -f compose.dev.yaml --profile cache \
5
+ # --profile storage --profile queue watch # with modules
6
+ #
7
+ # Only Traefik binds a host port (80). The app is at http://app.localhost.
8
+ # api/postgres/redis/minio have NO published ports — they talk by service name
9
+ # on the internal network, so this stack never collides with other projects.
10
+ #
11
+ # Core services (traefik, web, api, postgres) always run. Module-specific
12
+ # services are behind profiles — enable the ones your app uses:
13
+ # --profile cache redis (redis / rate-limit / bullmq modules)
14
+ # --profile storage minio (object-storage-s3 / file-upload modules)
15
+ # --profile queue worker (bullmq / job-progress modules)
16
+ #
17
+ # `watch` syncs your edits into the containers: the web has instant vite HMR;
18
+ # an API source change restarts the api service (~5s), which is the stable
19
+ # approach for nest inside a container.
20
+ name: {{projectName}}-dev
21
+
22
+ services:
23
+ traefik:
24
+ image: traefik:v3.5
25
+ command:
26
+ - --providers.file.filename=/etc/traefik/dynamic.yml
27
+ - --providers.file.watch=true
28
+ - --entrypoints.web.address=:80
29
+ - --api.dashboard=true
30
+ - --api.insecure=true
31
+ ports:
32
+ - "80:80"
33
+ - "127.0.0.1:8080:8080" # Traefik dashboard (dev only)
34
+ volumes:
35
+ - ./infra/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
36
+ networks: [podokit]
37
+
38
+ web:
39
+ build:
40
+ context: .
41
+ dockerfile: Dockerfile.dev
42
+ command: npm run dev -w {{projectName}}-web -- --host 0.0.0.0
43
+ working_dir: /app
44
+ env_file: .env.docker
45
+ environment:
46
+ - VITE_DOCKER=1
47
+ - BACKEND_INTERNAL_URL=http://api:5002
48
+ depends_on: [api]
49
+ networks: [podokit]
50
+ develop:
51
+ watch:
52
+ - { action: sync, path: ./apps/web/src, target: /app/apps/web/src }
53
+ - { action: sync, path: ./apps/web/static, target: /app/apps/web/static }
54
+ - { action: sync+restart, path: ./apps/web/vite.config.ts, target: /app/apps/web/vite.config.ts }
55
+ - { action: rebuild, path: ./apps/web/package.json }
56
+
57
+ api:
58
+ build:
59
+ context: .
60
+ dockerfile: Dockerfile.dev
61
+ init: true
62
+ command: npm run dev -w {{projectName}}-api
63
+ working_dir: /app
64
+ env_file: .env.docker
65
+ depends_on:
66
+ postgres: { condition: service_healthy }
67
+ networks: [podokit]
68
+ develop:
69
+ watch:
70
+ # nest-cli's in-container watch does not reliably respawn the app process,
71
+ # so restart the service on an API source change (~5s). The Docker docs'
72
+ # sync+restart is the stable, recommended approach; the web keeps instant
73
+ # HMR via plain sync.
74
+ - { action: sync+restart, path: ./apps/api/src, target: /app/apps/api/src }
75
+ - { action: rebuild, path: ./apps/api/package.json }
76
+
77
+ worker:
78
+ # started only with the bullmq / job-progress modules: --profile queue
79
+ profiles: [queue]
80
+ build:
81
+ context: .
82
+ dockerfile: Dockerfile.dev
83
+ init: true
84
+ command: npm run dev:worker -w {{projectName}}-api
85
+ working_dir: /app
86
+ env_file: .env.docker
87
+ depends_on:
88
+ redis: { condition: service_started }
89
+ networks: [podokit]
90
+ develop:
91
+ watch:
92
+ # nest-cli's in-container watch does not reliably respawn the app process,
93
+ # so restart the service on an API source change (~5s). The Docker docs'
94
+ # sync+restart is the stable, recommended approach; the web keeps instant
95
+ # HMR via plain sync.
96
+ - { action: sync+restart, path: ./apps/api/src, target: /app/apps/api/src }
97
+ - { action: rebuild, path: ./apps/api/package.json }
98
+
99
+ postgres:
100
+ image: postgres:16-alpine
101
+ environment:
102
+ POSTGRES_USER: podokit
103
+ POSTGRES_PASSWORD: podokit
104
+ POSTGRES_DB: podokit
105
+ healthcheck:
106
+ test: ["CMD-SHELL", "pg_isready -U podokit"]
107
+ interval: 3s
108
+ timeout: 3s
109
+ retries: 20
110
+ volumes:
111
+ - pgdata:/var/lib/postgresql/data
112
+ networks: [podokit] # no published ports — internal only
113
+
114
+ redis:
115
+ # started only with the redis / rate-limit / bullmq modules: --profile cache
116
+ profiles: [cache]
117
+ image: redis:7-alpine
118
+ networks: [podokit] # internal only
119
+
120
+ minio:
121
+ # started only with the object-storage-s3 / file-upload modules: --profile storage
122
+ profiles: [storage]
123
+ image: minio/minio:latest
124
+ command: server /data --console-address ":9001"
125
+ environment:
126
+ MINIO_ROOT_USER: podokit
127
+ MINIO_ROOT_PASSWORD: podokitsecret
128
+ volumes:
129
+ - miniodata:/data
130
+ networks: [podokit] # internal only
131
+
132
+ minio-init:
133
+ profiles: [storage]
134
+ image: minio/mc:latest
135
+ depends_on:
136
+ minio: { condition: service_started }
137
+ entrypoint:
138
+ - sh
139
+ - -c
140
+ - |
141
+ until mc alias set loc http://minio:9000 podokit podokitsecret; do sleep 1; done
142
+ mc mb -p loc/podokit || true
143
+ networks: [podokit]
144
+
145
+ networks:
146
+ podokit: {}
147
+
148
+ volumes:
149
+ pgdata: {}
150
+ miniodata: {}
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: podokit-add-module
3
+ description: Use when the user wants a feature that a PodoKit module already provides (authentication, an admin dashboard, Redis, background jobs/queues, object storage, file uploads, SSE, rate limiting, logging). Prefer adding the module over hand-writing it.
4
+ ---
5
+
6
+ # Add a PodoKit module
7
+
8
+ Before building auth, an admin panel, queues, storage, etc. by hand, check
9
+ whether a module provides it — modules wire themselves into the app correctly.
10
+
11
+ ```bash
12
+ podo add # list available modules
13
+ podo add <module> # e.g. auth, admin-dashboard, redis, bullmq, file-upload
14
+ npm install # install newly-added dependencies
15
+ ```
16
+
17
+ Common modules: `auth` (better-auth: email/password, sessions, OAuth, 2FA,
18
+ secure-by-default), `admin-dashboard` (user/session/org management + audit +
19
+ runtime settings, requires auth), `redis`, `bullmq` (queue + worker),
20
+ `job-progress` (SSE progress), `object-storage-s3`, `file-upload`, `sse`,
21
+ `rate-limit`, `logging`, `api-key-auth`.
22
+
23
+ Notes:
24
+ - A module auto-adds its required modules (e.g. `admin-dashboard` adds `auth`).
25
+ - After adding, follow the printed next steps (often DB migrations, e.g.
26
+ `npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts`).
27
+ - Module wiring lands inside `// podokit:` fenced regions — don't hand-edit those.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: podokit-nest-endpoint
3
+ description: Use when adding or changing a NestJS REST endpoint in apps/api — a controller/service with DTO validation, the standard error envelope, and a test. Follow this for any new backend route in a PodoKit project.
4
+ ---
5
+
6
+ # Add a NestJS endpoint (PodoKit conventions)
7
+
8
+ 1. **Module**: create/extend a Nest module under `apps/api/src/<feature>/`
9
+ (`<feature>.module.ts`, `.controller.ts`, `.service.ts`, `dto/`). Register the
10
+ module in `apps/api/src/app.module.ts` — add it **inside** the
11
+ `// podokit:begin:module-imports … end` fence, never outside it.
12
+ 2. **DTOs**: validate every input with `class-validator` decorators on a DTO
13
+ class. No `any`; give explicit return types.
14
+ 3. **Errors**: throw `AppException("STABLE_CODE", "message", statusCode)` (from
15
+ `@podosoft/podokit-contracts` / the app's `common/`). The global filter renders
16
+ it as `{ success: false, error: { code, message, statusCode, path, timestamp } }`.
17
+ Pick a stable, UPPER_SNAKE `code` — the frontend branches on it.
18
+ 4. **Auth**: if the `auth` module is installed the API is secure by default; mark
19
+ public routes with `@Public()` and read the user with `@Session()`.
20
+ 5. **Swagger**: annotate with `@ApiTags` so it shows under `/api-docs`.
21
+ 6. **Test**: add an e2e/unit test under `apps/api` or the `tests/` workspace
22
+ (Nest e2e or Vitest + supertest). Verify with
23
+ `npm run build -w {{projectName}}-api` and the test.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: podokit-sveltekit-route
3
+ description: Use when adding or changing a SvelteKit page/route in apps/web — pages that call the API, use shadcn-svelte components, and localize text. Follow this for any new frontend screen in a PodoKit project.
4
+ ---
5
+
6
+ # Add a SvelteKit route (PodoKit conventions)
7
+
8
+ 1. **Route**: add a folder under `apps/web/src/routes/`. Load data in
9
+ `+page.server.ts` / `+layout.server.ts` (SSR), not in the browser.
10
+ 2. **Call the API only through the server proxy** — never a direct browser
11
+ `fetch` to `:5002`. Use the app's `ApiClient`: `$lib/api.ts` (browser,
12
+ same-origin `/api/*` proxy) and `$lib/server/api.ts` (SSR, internal URL +
13
+ cookie forwarding). It throws `ApiError` — branch on `err.code`.
14
+ 3. **Svelte 5 runes only**: `$state`, `$derived`, `$effect`, `$props`. Never `$:`.
15
+ 4. **UI**: use shadcn-svelte components (`$lib/components/ui/*`) over raw HTML.
16
+ Do **not** edit `ui/**` — wrap it if you need to customize. Semantic Tailwind
17
+ classes (`bg-background`, `text-foreground`); responsive (`sm:`/`md:`/`lg:`).
18
+ For any list/table, use the shared `DataTable` (see the podokit-data-table
19
+ skill, added with the admin-dashboard module).
20
+ 5. **i18n**: no hardcoded user-facing strings — add keys to
21
+ `apps/web/src/lib/i18n/messages.ts` for **every** locale.
22
+ 6. **Verify**: `npm run build -w {{projectName}}-web` (includes `svelte-check`)
23
+ and add/adjust a Playwright test in `tests/`.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: podokit-update
3
+ description: Use when the user wants to update the project to a newer PodoKit version, pull in template/module improvements, check what they've changed vs the generated code, or take ownership of a managed file. Covers podo status/diff/doctor/update/eject.
4
+ ---
5
+
6
+ # Keep a PodoKit project up to date
7
+
8
+ The project records how it was assembled in `.podokit/` so the toolkit can update
9
+ its own files while keeping the user's changes.
10
+
11
+ ```bash
12
+ podo status # version, modules, file tiers, and how many managed files were edited
13
+ podo diff # list the PodoKit-managed files you've edited (owned files ignored)
14
+ podo doctor # check framework versions vs. the supported ranges
15
+ podo update # dry-run: preview what a newer version would change (writes nothing)
16
+ podo update --apply # apply it — clean updates are written; your edits are 3-way merged
17
+ podo eject <path> # take ownership of a managed file so update skips it
18
+ ```
19
+
20
+ Rules to respect:
21
+ - **owned** files (routes, your components, `AGENTS.md`, shadcn `ui/**`) are never
22
+ touched by `update`.
23
+ - **assembled** files are recomputed from the module set — keep edits **outside**
24
+ the `// podokit:begin…end` fences.
25
+ - Run `podo update` (dry-run) first and read the plan before `--apply`. Commit or
26
+ stash your work beforehand so a merge is easy to review.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: PodoKit project conventions (stack, commands, code style, tooling)
3
+ alwaysApply: true
4
+ ---
5
+
6
+ The conventions for this project are documented in [`AGENTS.md`](../../AGENTS.md)
7
+ at the repository root. Follow them: web runs on :5001 and api on :5002; use
8
+ shadcn-svelte components and the shared `DataTable`/`TableToolbar` for lists;
9
+ Svelte 5 runes only; branch on the error `code`, not the message; keep strings in
10
+ i18n; and respect the `.podokit/` file-ownership tiers (never hand-edit
11
+ `// podokit:` fenced regions or `src/lib/components/ui/**`).
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "{{projectName}} (containerized dev)",
3
+ "dockerComposeFile": ["../compose.dev.yaml"],
4
+ "service": "api",
5
+ "workspaceFolder": "/app",
6
+ "runServices": ["traefik", "web", "api", "worker", "postgres", "redis", "minio", "minio-init"],
7
+ "shutdownAction": "stopCompose",
8
+ "customizations": {
9
+ "vscode": {
10
+ "extensions": [
11
+ "svelte.svelte-vscode",
12
+ "dbaeumer.vscode-eslint",
13
+ "esbenp.prettier-vscode",
14
+ "bradlc.vscode-tailwindcss"
15
+ ]
16
+ }
17
+ },
18
+ "forwardPorts": [],
19
+ "remoteUser": "node"
20
+ }
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ **/node_modules
3
+ dist
4
+ **/dist
5
+ .svelte-kit
6
+ **/.svelte-kit
7
+ .git
8
+ *.log
@@ -0,0 +1,26 @@
1
+ NODE_ENV=development
2
+ PORT=5002
3
+ POSTGRES_HOST=postgres
4
+ POSTGRES_PORT=5432
5
+ POSTGRES_USER=podokit
6
+ POSTGRES_PASSWORD=podokit
7
+ POSTGRES_DB=podokit
8
+ BACKEND_INTERNAL_URL=http://api:5002
9
+ CORS_ORIGIN=http://app.localhost
10
+ REDIS_HOST=redis
11
+ REDIS_PORT=6379
12
+ BETTER_AUTH_SECRET=change-me-in-container-min-32-characters
13
+ BETTER_AUTH_URL=http://app.localhost
14
+ ADMIN_EMAILS=admin@example.com
15
+ AUDIT_LOG_ENABLED=true
16
+ RATE_LIMIT_TTL=60
17
+ RATE_LIMIT_MAX=100
18
+ API_KEYS=dev-key-please-change
19
+ LOG_LEVEL=info
20
+ STORAGE_PROVIDER=minio
21
+ S3_ENDPOINT=http://minio:9000
22
+ S3_REGION=us-east-1
23
+ S3_BUCKET=podokit
24
+ S3_ACCESS_KEY_ID=podokit
25
+ S3_SECRET_ACCESS_KEY=podokitsecret
26
+ S3_FORCE_PATH_STYLE=true
@@ -0,0 +1,6 @@
1
+ # Copilot instructions
2
+
3
+ This project's coding conventions for AI agents live in [`AGENTS.md`](../AGENTS.md).
4
+ Read it before making changes — it covers the stack, commands (web :5001 / api
5
+ :5002), TypeScript/Svelte 5/shadcn-svelte rules, the error-code envelope, and the
6
+ `podo` tooling.
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "podokit": {
4
+ "command": "npx",
5
+ "args": ["-y", "@podosoft/podokit-mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ # Traefik dynamic config (file provider). Routes the browser-facing web app;
2
+ # api/postgres/redis/minio stay internal (reached by service name).
3
+ http:
4
+ routers:
5
+ web:
6
+ rule: "PathPrefix(`/`)" # any host on this dedicated proxy -> web
7
+ entryPoints: [web]
8
+ service: web
9
+ services:
10
+ web:
11
+ loadBalancer:
12
+ servers:
13
+ - url: "http://web:5001"
@@ -0,0 +1,87 @@
1
+ /// <reference types="multer" />
2
+ import {
3
+ BadRequestException,
4
+ Body,
5
+ Controller,
6
+ ForbiddenException,
7
+ Get,
8
+ Post,
9
+ Put,
10
+ StreamableFile,
11
+ UploadedFile,
12
+ UseInterceptors,
13
+ } from "@nestjs/common";
14
+ import { FileInterceptor } from "@nestjs/platform-express";
15
+ import { ApiConsumes, ApiTags } from "@nestjs/swagger";
16
+ import { Public, Session, type UserSession } from "@thallesp/nestjs-better-auth";
17
+ import { PUBLIC_SITE_KEYS, SiteSettingsService } from "./site-settings.service";
18
+
19
+ function isAdmin(session: UserSession): boolean {
20
+ const role = session.user?.role;
21
+ const roles = Array.isArray(role) ? role : (role ?? "").split(",").map((r: string) => r.trim());
22
+ return roles.includes("admin");
23
+ }
24
+
25
+ const ALLOWED = new Set<string>(PUBLIC_SITE_KEYS);
26
+ const MAX_ICON_BYTES = 1024 * 1024; // 1 MB
27
+ const ICON_TYPES = new Set(["image/svg+xml", "image/png", "image/x-icon", "image/vnd.microsoft.icon"]);
28
+
29
+ @ApiTags("site")
30
+ @Controller("site")
31
+ export class SiteSettingsController {
32
+ constructor(private readonly site: SiteSettingsService) {}
33
+
34
+ /** Public: the site branding every page needs (title, favicon presence, …). */
35
+ @Public()
36
+ @Get("settings")
37
+ async settings(): Promise<Record<string, string | boolean | null>> {
38
+ const all = await this.site.getAll();
39
+ const out: Record<string, string | boolean | null> = {};
40
+ for (const key of PUBLIC_SITE_KEYS) out[key] = all[key] ?? null;
41
+ out.hasFavicon = (await this.site.get("faviconContentType")) !== null;
42
+ out.faviconVersion = await this.site.faviconVersion();
43
+ return out;
44
+ }
45
+
46
+ /** Admin: update one or more site settings. Unknown keys are rejected. */
47
+ @Put("settings")
48
+ async update(
49
+ @Session() session: UserSession,
50
+ @Body() body: Record<string, string>,
51
+ ): Promise<Record<string, string>> {
52
+ if (!isAdmin(session)) throw new ForbiddenException("Admins only");
53
+ const update: Record<string, string> = {};
54
+ for (const [key, value] of Object.entries(body ?? {})) {
55
+ if (!ALLOWED.has(key)) throw new BadRequestException(`Unknown setting: ${key}`);
56
+ update[key] = String(value ?? "");
57
+ }
58
+ return this.site.setMany(update);
59
+ }
60
+
61
+ /** Admin: upload the browser icon. Stored in object storage; served below. */
62
+ @Post("favicon")
63
+ @ApiConsumes("multipart/form-data")
64
+ @UseInterceptors(FileInterceptor("file"))
65
+ async uploadFavicon(
66
+ @Session() session: UserSession,
67
+ @UploadedFile() file?: Express.Multer.File,
68
+ ): Promise<{ version: string | null }> {
69
+ if (!isAdmin(session)) throw new ForbiddenException("Admins only");
70
+ if (!file) throw new BadRequestException("A file field is required (multipart/form-data)");
71
+ if (!ICON_TYPES.has(file.mimetype)) {
72
+ throw new BadRequestException("Icon must be SVG, PNG, or ICO");
73
+ }
74
+ if (file.size > MAX_ICON_BYTES) throw new BadRequestException("Icon must be 1 MB or smaller");
75
+ await this.site.setFavicon(file.buffer, file.mimetype);
76
+ return { version: await this.site.faviconVersion() };
77
+ }
78
+
79
+ /** Public: serve the current favicon from object storage (stable URL, no expiry). */
80
+ @Public()
81
+ @Get("favicon")
82
+ async favicon(): Promise<StreamableFile> {
83
+ const icon = await this.site.getFavicon();
84
+ if (!icon) throw new BadRequestException("No favicon set");
85
+ return new StreamableFile(icon.body, { type: icon.contentType });
86
+ }
87
+ }
@@ -0,0 +1,14 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { TypeOrmModule } from "@nestjs/typeorm";
3
+ import { AppSetting } from "../settings/app-setting.entity";
4
+ import { SiteSettingsController } from "./site-settings.controller";
5
+ import { SiteSettingsService } from "./site-settings.service";
6
+
7
+ // StorageService comes from the global StorageModule (object-storage-s3).
8
+ @Module({
9
+ imports: [TypeOrmModule.forFeature([AppSetting])],
10
+ controllers: [SiteSettingsController],
11
+ providers: [SiteSettingsService],
12
+ exports: [SiteSettingsService],
13
+ })
14
+ export class SiteSettingsModule {}