create-m5kdev 0.41.0 → 0.43.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 (83) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/.cursor/rules/frontend-component-guide.mdc +95 -95
  3. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +103 -103
  4. package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -80
  5. package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -79
  6. package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -45
  7. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -60
  8. package/templates/minimal-app/.cursor/rules/module-prompt-guide.mdc +214 -214
  9. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +167 -167
  10. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -53
  11. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +367 -367
  12. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -71
  13. package/templates/minimal-app/.github/workflows/check.yml.tpl +21 -0
  14. package/templates/minimal-app/AGENTS.md.tpl +22 -31
  15. package/templates/minimal-app/CLAUDE.md.tpl +17 -3
  16. package/templates/minimal-app/GLOSSARY.md.tpl +181 -0
  17. package/templates/minimal-app/README.md.tpl +14 -6
  18. package/templates/minimal-app/apps/.gitignore.tpl +13 -13
  19. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  20. package/templates/minimal-app/apps/e2e/servers.ts.tpl +22 -6
  21. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  22. package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -44
  23. package/templates/minimal-app/apps/expo/.prettierignore +13 -13
  24. package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -9
  25. package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -156
  26. package/templates/minimal-app/apps/expo/README.md.tpl +44 -44
  27. package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
  28. package/templates/minimal-app/apps/expo/package.json.tpl +63 -63
  29. package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
  30. package/templates/minimal-app/apps/landing/package.json.tpl +34 -34
  31. package/templates/minimal-app/apps/server/AGENTS.md.tpl +1 -1
  32. package/templates/minimal-app/apps/server/drizzle/0000_whole_echo.sql +366 -366
  33. package/templates/minimal-app/apps/server/drizzle/0002_crazy_lilith.sql +46 -46
  34. package/templates/minimal-app/apps/server/drizzle/0003_freezing_fantastic_four.sql +3 -3
  35. package/templates/minimal-app/apps/server/drizzle/0004_slow_ezekiel_stane.sql +98 -98
  36. package/templates/minimal-app/apps/server/drizzle/0005_wonderful_the_professor.sql +1 -1
  37. package/templates/minimal-app/apps/server/drizzle/0006_lazy_blue_marvel.sql +158 -158
  38. package/templates/minimal-app/apps/server/drizzle/0007_abnormal_ultimo.sql +1 -1
  39. package/templates/minimal-app/apps/server/drizzle/0008_steady_nekra.sql +86 -86
  40. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +2624 -2624
  41. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +2670 -2670
  42. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +2715 -2715
  43. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2687 -2687
  44. package/templates/minimal-app/apps/server/drizzle/meta/0004_snapshot.json.tpl +2497 -2497
  45. package/templates/minimal-app/apps/server/drizzle/meta/0005_snapshot.json.tpl +2483 -2483
  46. package/templates/minimal-app/apps/server/drizzle/meta/0006_snapshot.json.tpl +3602 -3602
  47. package/templates/minimal-app/apps/server/drizzle/meta/0007_snapshot.json.tpl +3619 -3619
  48. package/templates/minimal-app/apps/server/drizzle/meta/0008_snapshot.json.tpl +3619 -3619
  49. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +76 -76
  50. package/templates/minimal-app/apps/server/drizzle-postgresql/0000_glorious_warhawk.sql +522 -0
  51. package/templates/minimal-app/apps/server/drizzle-postgresql/meta/0000_snapshot.json.tpl +3555 -0
  52. package/templates/minimal-app/apps/server/drizzle-postgresql/meta/_journal.json.tpl +13 -0
  53. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +1 -1
  54. package/templates/minimal-app/apps/server/mastra.ts.tpl +42 -0
  55. package/templates/minimal-app/apps/server/package.json.tpl +9 -4
  56. package/templates/minimal-app/apps/server/src/app.ts.tpl +21 -16
  57. package/templates/minimal-app/apps/server/src/drizzle.schema.ts.tpl +2 -0
  58. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +48 -0
  59. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.persistence.ts.tpl +47 -0
  60. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +7 -6
  61. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +2 -2
  62. package/templates/minimal-app/apps/shared/.env.example.tpl +6 -3
  63. package/templates/minimal-app/apps/shared/.env.production.example.tpl +7 -2
  64. package/templates/minimal-app/apps/shared/.env.tpl +6 -3
  65. package/templates/minimal-app/apps/shared/Dockerfile.pgbouncer +6 -0
  66. package/templates/minimal-app/apps/shared/compose.yaml.tpl +27 -0
  67. package/templates/minimal-app/apps/shared/fly.toml.tpl +0 -4
  68. package/templates/minimal-app/apps/shared/package.json.tpl +27 -27
  69. package/templates/minimal-app/apps/shared/pgbouncer.ini +13 -0
  70. package/templates/minimal-app/apps/shared/pgbouncer.users +1 -0
  71. package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -24
  72. package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
  73. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  74. package/templates/minimal-app/apps/webapp/public/logo.svg.tpl +6 -6
  75. package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +1 -1
  76. package/templates/minimal-app/biome.json.tpl +1 -0
  77. package/templates/minimal-app/docs/adr/.gitkeep +0 -0
  78. package/templates/minimal-app/docs/agents/domain.md.tpl +38 -0
  79. package/templates/minimal-app/docs/agents/issue-tracker.md.tpl +45 -0
  80. package/templates/minimal-app/docs/agents/triage-labels.md.tpl +15 -0
  81. package/templates/minimal-app/package.json.tpl +5 -0
  82. package/templates/minimal-app/pnpm-workspace.yaml.tpl +16 -13
  83. package/templates/minimal-app/template.manifest.json +21 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1793634297401,
9
+ "tag": "0000_glorious_warhawk",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -1,3 +1,3 @@
1
1
  import { defineDrizzleKitConfig } from "@m5kdev/backend/db";
2
2
 
3
- export default defineDrizzleKitConfig({ schema: "./src/schema.ts" });
3
+ export default defineDrizzleKitConfig({ schema: "./src/drizzle.schema.ts" });
@@ -0,0 +1,42 @@
1
+ import type { PgVector } from "@mastra/pg";
2
+ import {
3
+ conversationEmbeddingDimensions,
4
+ conversationEmbeddingIndex,
5
+ conversationEmbeddingMetric,
6
+ createConversationStorage,
7
+ createConversationVectorProvisioner,
8
+ } from "./src/modules/conversation/conversation.persistence";
9
+
10
+ async function initialize(): Promise<void> {
11
+ const storage = createConversationStorage(true);
12
+ let vector: PgVector | undefined;
13
+ try {
14
+ vector = createConversationVectorProvisioner();
15
+ const extension = await vector.pool.query<{ schema: string }>(
16
+ "SELECT n.nspname AS schema FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace WHERE e.extname = 'vector'"
17
+ );
18
+ if (extension.rows[0]?.schema !== "public") {
19
+ throw new Error(
20
+ "Provision pgvector in the public schema before Mastra initialization (required for transaction pooling)"
21
+ );
22
+ }
23
+ await storage.init();
24
+ await vector.createIndex({
25
+ indexName: conversationEmbeddingIndex,
26
+ dimension: conversationEmbeddingDimensions,
27
+ metric: conversationEmbeddingMetric,
28
+ buildIndex: false,
29
+ });
30
+ } finally {
31
+ try {
32
+ await vector?.disconnect();
33
+ } finally {
34
+ await storage.close();
35
+ }
36
+ }
37
+ }
38
+
39
+ void initialize().catch((error) => {
40
+ console.error(error);
41
+ process.exitCode = 1;
42
+ });
@@ -11,19 +11,23 @@
11
11
  "lint:fix": "biome check . --write",
12
12
  "check-types": "tsc --noEmit",
13
13
  "drizzle:generate": "drizzle-kit generate",
14
- "drizzle:migrate": "drizzle-kit migrate",
15
- "drizzle:studio": "drizzle-kit studio",
16
- "drizzle:sync": "tsx --env-file=../shared/.env db.ts sync",
14
+ "drizzle:migrate": "tsx --env-file=../shared/.env db.ts migrate",
15
+ "drizzle:studio": "drizzle-kit studio",
16
+
17
+ "mastra:init": "tsx --env-file=../shared/.env mastra.ts",
18
+ // m5k:ai:end
17
19
  "drizzle:seed": "tsx --env-file=../shared/.env db.ts seed",
18
20
  "drizzle:reset": "tsx --env-file=../shared/.env db.ts reset"
19
21
  },
20
22
  "dependencies": {
21
23
  "{{PACKAGE_SCOPE}}/email": "workspace:*",
22
24
  "{{PACKAGE_SCOPE}}/shared": "workspace:*",
23
- "@libsql/client": "catalog:m5kdev",
24
25
  "@m5kdev/backend": "catalog:m5kdev",
25
26
  "@m5kdev/commons": "catalog:m5kdev",
26
27
  "@mastra/core": "catalog:m5kdev",
28
+ "@mastra/pg": "catalog:m5kdev",
29
+ "@mastra/memory": "catalog:m5kdev",
30
+ "pg": "catalog:m5kdev",
27
31
  "@openrouter/ai-sdk-provider": "catalog:m5kdev",
28
32
  "@opentelemetry/api": "catalog:m5kdev",
29
33
  "@trpc/server": "catalog:m5kdev",
@@ -43,6 +47,7 @@
43
47
  "@m5kdev/config": "catalog:m5kdev",
44
48
  "@types/express": "catalog:m5kdev",
45
49
  "@types/node": "catalog:m5kdev",
50
+ "@types/pg": "catalog:m5kdev",
46
51
  "@types/react": "catalog:m5kdev",
47
52
  "@types/react-dom": "catalog:m5kdev",
48
53
  "drizzle-kit": "catalog:m5kdev",
@@ -1,6 +1,8 @@
1
+ // biome-ignore-all assist/source/organizeImports: feature-gated module imports stay in marker blocks
1
2
  import { createBackendApp } from "@m5kdev/backend/app";
2
3
  import { shutdownTelemetry } from "@m5kdev/backend/lib/otel";
3
4
  // m5k:ai:start
5
+ import { defineBackendModule } from "@m5kdev/backend/app";
4
6
  import { AIModule } from "@m5kdev/backend/modules/ai/ai.module";
5
7
  // m5k:ai:end
6
8
  import { createBetterAuth } from "@m5kdev/backend/modules/auth/auth.lib";
@@ -30,7 +32,13 @@ import {
30
32
  import { NOTIFICATION_KINDS } from "{{PACKAGE_SCOPE}}/shared/modules/notification/notification.constants";
31
33
  // m5k:notifications:end
32
34
  // m5k:ai:start
33
- import { mastra, openrouter } from "./modules/conversation/conversation.mastra";
35
+ import {
36
+ conversationStorage,
37
+ mastra,
38
+ openrouter,
39
+ verifyConversationPersistence,
40
+ } from "./modules/conversation/conversation.mastra";
41
+ import { conversationVectorSchema } from "./modules/conversation/conversation.persistence";
34
42
  // m5k:ai:end
35
43
  // m5k:workflows:start
36
44
  import { DemoWorkflowModule } from "./modules/demo-workflow/demo-workflow.module";
@@ -43,27 +51,15 @@ import * as schema from "./schema";
43
51
 
44
52
  const appUrl = process.env.VITE_APP_URL ?? "http://localhost:5173";
45
53
  const serverUrl = process.env.VITE_SERVER_URL ?? "http://localhost:8080";
46
- const databaseUrl = process.env.DATABASE_URL ?? "file:./local.db";
47
- const syncUrl = process.env.TURSO_DATABASE_URL;
48
- const authToken = process.env.TURSO_AUTH_TOKEN;
54
+ const databaseUrl = process.env.DATABASE_URL;
55
+ if (!databaseUrl) throw new Error("DATABASE_URL must point to the runtime PgBouncer connection");
49
56
  // m5k:workflows:start
50
57
  const redisUrl = process.env.REDIS_URL ?? "redis://127.0.0.1:6379";
51
58
  // m5k:workflows:end
52
59
  const resendApiKey = process.env.RESEND_API_KEY;
53
60
  const enableWaitlist = process.env.VITE_ENABLE_WAITLIST === "true";
54
61
 
55
- const connection =
56
- syncUrl && authToken
57
- ? {
58
- url: databaseUrl,
59
- syncUrl,
60
- authToken,
61
- syncInterval: 60,
62
- readYourWrites: true,
63
- }
64
- : {
65
- url: databaseUrl,
66
- };
62
+ const connection = { connectionString: databaseUrl };
67
63
 
68
64
  export const builtBackendApp = createBackendApp(
69
65
  {
@@ -128,6 +124,15 @@ export const builtBackendApp = createBackendApp(
128
124
  // m5k:ai:start
129
125
  new AIModule({
130
126
  libs: { mastra, openrouter },
127
+ vectorStore: {
128
+ connectionString: process.env.VECTOR_DATABASE_URL || databaseUrl,
129
+ schemaName: conversationVectorSchema,
130
+ },
131
+ }),
132
+ defineBackendModule({
133
+ id: "conversation-persistence",
134
+ startup: verifyConversationPersistence,
135
+ shutdown: () => conversationStorage.close(),
131
136
  }),
132
137
  // m5k:ai:end
133
138
  // m5k:files:start
@@ -0,0 +1,2 @@
1
+ export { appDatabaseSchema } from "@m5kdev/backend/base/database.schema";
2
+ export * from "./schema";
@@ -1,7 +1,53 @@
1
1
  import { Mastra } from "@mastra/core";
2
2
  import { Agent } from "@mastra/core/agent";
3
+ import { Memory } from "@mastra/memory";
3
4
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
4
5
  import { STARTER_ASSISTANT_AGENT_ID } from "{{PACKAGE_SCOPE}}/shared/modules/conversation/conversation.constants";
6
+ import {
7
+ conversationEmbeddingDimensions,
8
+ conversationEmbeddingIndex,
9
+ conversationEmbeddingMetric,
10
+ conversationStorageSchema,
11
+ conversationVectorSchema,
12
+ createConversationStorage,
13
+ } from "./conversation.persistence";
14
+
15
+ export const conversationStorage = createConversationStorage();
16
+
17
+ export async function verifyConversationPersistence(): Promise<void> {
18
+ const tables = await conversationStorage.pool.query<{ ready: boolean }>(
19
+ "SELECT to_regclass(format('%I.%I', $1::text, 'mastra_threads')) IS NOT NULL AND to_regclass(format('%I.%I', $1::text, 'mastra_messages')) IS NOT NULL AS ready",
20
+ [conversationStorageSchema]
21
+ );
22
+ if (!tables.rows[0]?.ready)
23
+ throw new Error(
24
+ "Mastra storage is not ready; run mastra:init using the direct connection before startup"
25
+ );
26
+ const { PgVector } = await import("@mastra/pg");
27
+ const vector = new PgVector({
28
+ id: "starter-vector-readiness",
29
+ connectionString: process.env.VECTOR_DATABASE_URL || process.env.DATABASE_URL!,
30
+ schemaName: conversationVectorSchema,
31
+ disableInit: true,
32
+ });
33
+ try {
34
+ const extension = await vector.pool.query<{ ready: boolean }>(
35
+ "SELECT EXISTS(SELECT 1 FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace WHERE e.extname = 'vector' AND n.nspname = 'public') AS ready"
36
+ );
37
+ if (!extension.rows[0]?.ready)
38
+ throw new Error(
39
+ "Provision pgvector in public before startup (transaction pooling requires this)"
40
+ );
41
+ const index = await vector.describeIndex({ indexName: conversationEmbeddingIndex });
42
+ if (
43
+ index.dimension !== conversationEmbeddingDimensions ||
44
+ index.metric !== conversationEmbeddingMetric
45
+ )
46
+ throw new Error("Starter vector configuration differs from the provisioned index");
47
+ } finally {
48
+ await vector.disconnect();
49
+ }
50
+ }
5
51
 
6
52
  /** Cheap OpenRouter chat model for the starter Conversation example. */
7
53
  const STARTER_CHAT_MODEL = "google/gemini-2.5-flash";
@@ -14,12 +60,14 @@ const assistant = new Agent({
14
60
  id: STARTER_ASSISTANT_AGENT_ID,
15
61
  name: "Assistant",
16
62
  instructions: "You are a helpful assistant for this starter app. Keep answers concise.",
63
+ memory: new Memory({ storage: conversationStorage, options: { semanticRecall: false } }),
17
64
  model: openrouter.chat(STARTER_CHAT_MODEL, {
18
65
  usage: { include: true },
19
66
  }),
20
67
  });
21
68
 
22
69
  export const mastra = new Mastra({
70
+ storage: conversationStorage,
23
71
  agents: {
24
72
  [STARTER_ASSISTANT_AGENT_ID]: assistant,
25
73
  },
@@ -0,0 +1,47 @@
1
+ import { PgVector, PostgresStore } from "@mastra/pg";
2
+
3
+ export const conversationStorageSchema = process.env.MASTRA_DATABASE_SCHEMA ?? "mastra";
4
+ export const conversationVectorSchema = process.env.VECTOR_DATABASE_SCHEMA ?? "vectors";
5
+ export const conversationEmbeddingIndex = "mastra_embeddings";
6
+ export const conversationEmbeddingModel = "openai/text-embedding-3-small";
7
+ export const conversationEmbeddingDimensions = 1536;
8
+ export const conversationEmbeddingMetric = "cosine" as const;
9
+
10
+ export function createConversationStorage(initialize = false): PostgresStore {
11
+ const appSchema = process.env.DATABASE_SCHEMA ?? "app";
12
+ if (
13
+ conversationStorageSchema === appSchema ||
14
+ conversationVectorSchema === appSchema ||
15
+ conversationStorageSchema === conversationVectorSchema
16
+ ) {
17
+ throw new Error("App, Mastra storage, and vectors must use separately owned schemas");
18
+ }
19
+ const connectionString = initialize
20
+ ? process.env.MASTRA_DATABASE_DIRECT_URL || process.env.DATABASE_DIRECT_URL
21
+ : process.env.MASTRA_DATABASE_URL || process.env.DATABASE_URL;
22
+ if (!connectionString) {
23
+ throw new Error(
24
+ initialize
25
+ ? "DATABASE_DIRECT_URL is required for Mastra initialization"
26
+ : "DATABASE_URL is required for Mastra runtime storage"
27
+ );
28
+ }
29
+ return new PostgresStore({
30
+ id: "starter-mastra",
31
+ connectionString,
32
+ schemaName: conversationStorageSchema,
33
+ disableInit: !initialize,
34
+ });
35
+ }
36
+
37
+ export function createConversationVectorProvisioner(): PgVector {
38
+ const connectionString =
39
+ process.env.VECTOR_DATABASE_DIRECT_URL || process.env.DATABASE_DIRECT_URL;
40
+ if (!connectionString)
41
+ throw new Error("DATABASE_DIRECT_URL is required for vector initialization");
42
+ return new PgVector({
43
+ id: "starter-vector-init",
44
+ connectionString,
45
+ schemaName: conversationVectorSchema,
46
+ });
47
+ }
@@ -1,8 +1,9 @@
1
+ import { appDatabaseSchema } from "@m5kdev/backend/base/database.schema";
1
2
  import { members, organizations, users } from "@m5kdev/backend/modules/auth/auth.db";
2
- import { integer, sqliteTable as table, text } from "drizzle-orm/sqlite-core";
3
+ import { text, timestamp } from "drizzle-orm/pg-core";
3
4
  import { v4 as uuidv4 } from "uuid";
4
5
 
5
- export const posts = table("posts", {
6
+ export const posts = appDatabaseSchema.table("posts", {
6
7
  id: text("id").primaryKey().$default(uuidv4),
7
8
  authorUserId: text("author_user_id").references(() => users.id, {
8
9
  onDelete: "set null",
@@ -19,10 +20,10 @@ export const posts = table("posts", {
19
20
  excerpt: text("excerpt"),
20
21
  content: text("content").notNull(),
21
22
  status: text("status").$type<"draft" | "published">().notNull().default("draft"),
22
- publishedAt: integer("published_at", { mode: "timestamp" }),
23
- createdAt: integer("created_at", { mode: "timestamp" })
23
+ publishedAt: timestamp("published_at", { withTimezone: true, mode: "date" }),
24
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" })
24
25
  .notNull()
25
26
  .$default(() => new Date()),
26
- updatedAt: integer("updated_at", { mode: "timestamp" }),
27
- deletedAt: integer("deleted_at", { mode: "timestamp" }),
27
+ updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }),
28
+ deletedAt: timestamp("deleted_at", { withTimezone: true, mode: "date" }),
28
29
  });
@@ -1,7 +1,7 @@
1
1
  import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
2
2
 
3
3
  import { and, eq, isNull, ne } from "drizzle-orm";
4
- import type { LibSQLDatabase } from "drizzle-orm/libsql";
4
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
5
5
  import { err, ok } from "neverthrow";
6
6
  import { z } from "zod";
7
7
  import { posts } from "./posts.db";
@@ -9,7 +9,7 @@ import { postSchemas } from "./posts.dto";
9
9
 
10
10
  const schema = { posts };
11
11
  type Schema = typeof schema;
12
- type Orm = LibSQLDatabase<Schema>;
12
+ type Orm = NodePgDatabase<Schema>;
13
13
 
14
14
  export class PostsRepository extends BaseTableRepository<
15
15
  Orm,
@@ -8,9 +8,12 @@ VITE_SERVER_URL=http://localhost:8080
8
8
  VITE_ENABLE_WAITLIST=false
9
9
 
10
10
  # Database
11
- DATABASE_URL=file:./local.db
12
- TURSO_DATABASE_URL=
13
- TURSO_AUTH_TOKEN=
11
+ DATABASE_URL=postgresql://m5kdev:m5kdev@127.0.0.1:6432/m5kdev
12
+ DATABASE_DIRECT_URL=postgresql://m5kdev:m5kdev@127.0.0.1:5432/m5kdev
13
+ DATABASE_SCHEMA=app
14
+ DATABASE_DISPOSABLE=false
15
+ MASTRA_DATABASE_SCHEMA=mastra
16
+ VECTOR_DATABASE_SCHEMA=vectors
14
17
 
15
18
  # Redis (BullMQ / workflow workers)
16
19
  REDIS_URL=redis://127.0.0.1:6379
@@ -2,8 +2,13 @@
2
2
  # image (.dockerignore excludes .env*). app:deploy forwards every key as a Fly
3
3
  # build secret; app:secrets imports the same file as runtime secrets.
4
4
 
5
- # Database (Fly volume at /app/data)
6
- DATABASE_URL=file:/app/data/local.db
5
+ # PostgreSQL: runtime transaction pool and direct migration/admin connection
6
+ DATABASE_URL=
7
+ DATABASE_DIRECT_URL=
8
+ DATABASE_SCHEMA=app
9
+ DATABASE_DISPOSABLE=false
10
+ MASTRA_DATABASE_SCHEMA=mastra
11
+ VECTOR_DATABASE_SCHEMA=vectors
7
12
 
8
13
  # Redis — set as a Fly runtime secret (do not bake into the image)
9
14
  REDIS_URL=
@@ -8,9 +8,12 @@ VITE_SERVER_URL=http://localhost:8080
8
8
  VITE_ENABLE_WAITLIST=false
9
9
 
10
10
  # Database
11
- DATABASE_URL=file:./local.db
12
- TURSO_DATABASE_URL=
13
- TURSO_AUTH_TOKEN=
11
+ DATABASE_URL=postgresql://m5kdev:m5kdev@127.0.0.1:6432/m5kdev
12
+ DATABASE_DIRECT_URL=postgresql://m5kdev:m5kdev@127.0.0.1:5432/m5kdev
13
+ DATABASE_SCHEMA=app
14
+ DATABASE_DISPOSABLE=false
15
+ MASTRA_DATABASE_SCHEMA=mastra
16
+ VECTOR_DATABASE_SCHEMA=vectors
14
17
 
15
18
  # m5k:ai:start
16
19
  # Optional OpenRouter (AI Conversation). Required to get an assistant reply.
@@ -0,0 +1,6 @@
1
+ FROM debian:bookworm-slim
2
+ RUN apt-get update && apt-get install -y --no-install-recommends pgbouncer && rm -rf /var/lib/apt/lists/*
3
+ COPY pgbouncer.ini /etc/pgbouncer/pgbouncer.ini
4
+ COPY pgbouncer.users /etc/pgbouncer/users.txt
5
+ USER nobody
6
+ CMD ["pgbouncer", "/etc/pgbouncer/pgbouncer.ini"]
@@ -0,0 +1,27 @@
1
+ services:
2
+ postgres:
3
+ image: pgvector/pgvector:pg17
4
+ environment:
5
+ POSTGRES_DB: m5kdev
6
+ POSTGRES_USER: m5kdev
7
+ POSTGRES_PASSWORD: m5kdev
8
+ ports:
9
+ - "127.0.0.1:5432:5432"
10
+ volumes:
11
+ - postgres_data:/var/lib/postgresql/data
12
+ healthcheck:
13
+ test: ["CMD-SHELL", "pg_isready -U m5kdev -d m5kdev"]
14
+ interval: 2s
15
+ timeout: 2s
16
+ retries: 30
17
+ pgbouncer:
18
+ build:
19
+ context: .
20
+ dockerfile: Dockerfile.pgbouncer
21
+ depends_on:
22
+ postgres:
23
+ condition: service_healthy
24
+ ports:
25
+ - "127.0.0.1:6432:6432"
26
+ volumes:
27
+ postgres_data:
@@ -6,10 +6,6 @@ primary_region = "iad"
6
6
  [env]
7
7
  PORT = "8080"
8
8
 
9
- [[mounts]]
10
- source = "libsql_data"
11
- destination = "/app/data"
12
-
13
9
  [http_service]
14
10
  internal_port = 8080
15
11
  force_https = true
@@ -1,27 +1,27 @@
1
- {
2
- "name": "{{PACKAGE_SCOPE}}/shared",
3
- "version": "0.0.0",
4
- "private": true,
5
- "scripts": {
6
- "build": "tsc --noEmit",
7
- "lint": "biome check .",
8
- "lint:fix": "biome check . --write",
9
- "check-types": "tsc --noEmit"
10
- },
11
- "dependencies": {
12
- "@m5kdev/commons": "catalog:m5kdev",
13
- "zod": "catalog:m5kdev"
14
- },
15
- "devDependencies": {
16
- "@m5kdev/config": "catalog:m5kdev",
17
- "tslib": "catalog:m5kdev",
18
- "typescript": "catalog:m5kdev"
19
- },
20
- "exports": {
21
- "./modules/app/*": "./src/modules/app/*.ts",
22
- "./modules/conversation/*": "./src/modules/conversation/*.ts",
23
- "./modules/demo-workflow/*": "./src/modules/demo-workflow/*.ts",
24
- "./modules/notification/*": "./src/modules/notification/*.ts",
25
- "./modules/posts/*": "./src/modules/posts/*.ts"
26
- }
27
- }
1
+ {
2
+ "name": "{{PACKAGE_SCOPE}}/shared",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "build": "tsc --noEmit",
7
+ "lint": "biome check .",
8
+ "lint:fix": "biome check . --write",
9
+ "check-types": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@m5kdev/commons": "catalog:m5kdev",
13
+ "zod": "catalog:m5kdev"
14
+ },
15
+ "devDependencies": {
16
+ "@m5kdev/config": "catalog:m5kdev",
17
+ "tslib": "catalog:m5kdev",
18
+ "typescript": "catalog:m5kdev"
19
+ },
20
+ "exports": {
21
+ "./modules/app/*": "./src/modules/app/*.ts",
22
+ "./modules/conversation/*": "./src/modules/conversation/*.ts",
23
+ "./modules/demo-workflow/*": "./src/modules/demo-workflow/*.ts",
24
+ "./modules/notification/*": "./src/modules/notification/*.ts",
25
+ "./modules/posts/*": "./src/modules/posts/*.ts"
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ [databases]
2
+ m5kdev = host=postgres port=5432 dbname=m5kdev
3
+
4
+ [pgbouncer]
5
+ listen_addr = 0.0.0.0
6
+ listen_port = 6432
7
+ unix_socket_dir = /tmp
8
+ auth_type = plain
9
+ auth_file = /etc/pgbouncer/users.txt
10
+ pool_mode = transaction
11
+ default_pool_size = 10
12
+ max_client_conn = 100
13
+ pidfile = /tmp/pgbouncer.pid
@@ -0,0 +1 @@
1
+ "m5kdev" "m5kdev"
@@ -1,24 +1,24 @@
1
- # AGENTS.md
2
-
3
- ## Frontend Stack
4
-
5
- - React 19 + TypeScript
6
- - React Router v7
7
- - HeroUI v3
8
- - Tailwind CSS v4
9
- - `nuqs` for URL state
10
- - TanStack Query + tRPC
11
- - i18next for all user-facing copy
12
-
13
- ## Conventions
14
-
15
- - Keep providers centralized in `src/Providers.tsx`.
16
- - Keep routing in `src/Router.tsx`.
17
- - Keep feature code grouped under `src/modules/<feature>/` with `components/` and `hooks/` subfolders.
18
- - Prefer HeroUI primitives and existing framework utilities before adding custom abstractions.
19
- - Components stay display-focused: queries, mutations, URL state, and effects live in custom hooks (`hooks/use<Feature>Route.ts` composed from smaller hooks); ephemeral UI state (modal open, selected row) may stay in the component. Reusable tRPC actions (delete, publish) get their own hook.
20
- - Derive values during render instead of syncing state with `useEffect`.
21
- - Forms are uncontrolled: HeroUI `Form` + `TextField` with native HTML validation and `FieldError`, values read from `FormData` on submit. No form libraries. Extra checks that HTML validation cannot express happen in the submit handler.
22
- - Fetch through `useTRPC()` + `queryOptions`/`mutationOptions` only; invalidate with `queryFilter()` after mutations.
23
- - Use `useTranslation` for local app copy and rely on the bundled `web-ui` translations for auth routes.
24
- - The `posts` module is the reference implementation; detailed guides live in `.cursor/rules/frontend-*.mdc`.
1
+ # AGENTS.md
2
+
3
+ ## Frontend Stack
4
+
5
+ - React 19 + TypeScript
6
+ - React Router v7
7
+ - HeroUI v3
8
+ - Tailwind CSS v4
9
+ - `nuqs` for URL state
10
+ - TanStack Query + tRPC
11
+ - i18next for all user-facing copy
12
+
13
+ ## Conventions
14
+
15
+ - Keep providers centralized in `src/Providers.tsx`.
16
+ - Keep routing in `src/Router.tsx`.
17
+ - Keep feature code grouped under `src/modules/<feature>/` with `components/` and `hooks/` subfolders.
18
+ - Prefer HeroUI primitives and existing framework utilities before adding custom abstractions.
19
+ - Components stay display-focused: queries, mutations, URL state, and effects live in custom hooks (`hooks/use<Feature>Route.ts` composed from smaller hooks); ephemeral UI state (modal open, selected row) may stay in the component. Reusable tRPC actions (delete, publish) get their own hook.
20
+ - Derive values during render instead of syncing state with `useEffect`.
21
+ - Forms are uncontrolled: HeroUI `Form` + `TextField` with native HTML validation and `FieldError`, values read from `FormData` on submit. No form libraries. Extra checks that HTML validation cannot express happen in the submit handler.
22
+ - Fetch through `useTRPC()` + `queryOptions`/`mutationOptions` only; invalidate with `queryFilter()` after mutations.
23
+ - Use `useTranslation` for local app copy and rely on the bundled `web-ui` translations for auth routes.
24
+ - The `posts` module is the reference implementation; detailed guides live in `.cursor/rules/frontend-*.mdc`.
@@ -1,22 +1,22 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta
6
- name="viewport"
7
- content="width=device-width, initial-scale=1.0"
8
- />
9
- <meta
10
- name="description"
11
- content="The m5kdev starter application."
12
- />
13
- <title>{{APP_NAME}}</title>
14
- </head>
15
- <body>
16
- <div id="root"></div>
17
- <script
18
- type="module"
19
- src="/src/main.tsx"
20
- ></script>
21
- </body>
22
- </html>
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0"
8
+ />
9
+ <meta
10
+ name="description"
11
+ content="The m5kdev starter application."
12
+ />
13
+ <title>{{APP_NAME}}</title>
14
+ </head>
15
+ <body>
16
+ <div id="root"></div>
17
+ <script
18
+ type="module"
19
+ src="/src/main.tsx"
20
+ ></script>
21
+ </body>
22
+ </html>