create-m5kdev 0.41.1 → 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 (29) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/CLAUDE.md.tpl +2 -2
  3. package/templates/minimal-app/README.md.tpl +12 -6
  4. package/templates/minimal-app/apps/e2e/servers.ts.tpl +22 -6
  5. package/templates/minimal-app/apps/server/AGENTS.md.tpl +1 -1
  6. package/templates/minimal-app/apps/server/drizzle-postgresql/0000_glorious_warhawk.sql +522 -0
  7. package/templates/minimal-app/apps/server/drizzle-postgresql/meta/0000_snapshot.json.tpl +3555 -0
  8. package/templates/minimal-app/apps/server/drizzle-postgresql/meta/_journal.json.tpl +13 -0
  9. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +1 -1
  10. package/templates/minimal-app/apps/server/mastra.ts.tpl +42 -0
  11. package/templates/minimal-app/apps/server/package.json.tpl +62 -57
  12. package/templates/minimal-app/apps/server/src/app.ts.tpl +20 -16
  13. package/templates/minimal-app/apps/server/src/drizzle.schema.ts.tpl +2 -0
  14. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +48 -0
  15. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.persistence.ts.tpl +47 -0
  16. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +7 -6
  17. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +2 -2
  18. package/templates/minimal-app/apps/shared/.env.example.tpl +6 -3
  19. package/templates/minimal-app/apps/shared/.env.production.example.tpl +7 -2
  20. package/templates/minimal-app/apps/shared/.env.tpl +6 -3
  21. package/templates/minimal-app/apps/shared/Dockerfile.pgbouncer +6 -0
  22. package/templates/minimal-app/apps/shared/compose.yaml.tpl +27 -0
  23. package/templates/minimal-app/apps/shared/fly.toml.tpl +0 -4
  24. package/templates/minimal-app/apps/shared/pgbouncer.ini +13 -0
  25. package/templates/minimal-app/apps/shared/pgbouncer.users +1 -0
  26. package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +1 -1
  27. package/templates/minimal-app/package.json.tpl +5 -0
  28. package/templates/minimal-app/pnpm-workspace.yaml.tpl +16 -13
  29. package/templates/minimal-app/template.manifest.json +9 -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
+ });
@@ -1,57 +1,62 @@
1
- {
2
- "name": "{{PACKAGE_SCOPE}}/server",
3
- "version": "0.0.0",
4
- "private": true,
5
- "main": "dist/index.js",
6
- "scripts": {
7
- "dev": "tsx watch --env-file=../shared/.env src/index.ts",
8
- "build": "tsup",
9
- "start": "node dist/index.js",
10
- "lint": "biome check .",
11
- "lint:fix": "biome check . --write",
12
- "check-types": "tsc --noEmit",
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",
17
- "drizzle:seed": "tsx --env-file=../shared/.env db.ts seed",
18
- "drizzle:reset": "tsx --env-file=../shared/.env db.ts reset"
19
- },
20
- "dependencies": {
21
- "{{PACKAGE_SCOPE}}/email": "workspace:*",
22
- "{{PACKAGE_SCOPE}}/shared": "workspace:*",
23
- "@libsql/client": "catalog:m5kdev",
24
- "@m5kdev/backend": "catalog:m5kdev",
25
- "@m5kdev/commons": "catalog:m5kdev",
26
- "@mastra/core": "catalog:m5kdev",
27
- "@openrouter/ai-sdk-provider": "catalog:m5kdev",
28
- "@opentelemetry/api": "catalog:m5kdev",
29
- "@trpc/server": "catalog:m5kdev",
30
- "better-auth": "catalog:m5kdev",
31
- "dotenv": "catalog:m5kdev",
32
- "drizzle-orm": "catalog:m5kdev",
33
- "drizzle-zod": "catalog:m5kdev",
34
- "express": "catalog:m5kdev",
35
- "ioredis": "catalog:m5kdev",
36
- "neverthrow": "catalog:m5kdev",
37
- "react": "catalog:m5kdev",
38
- "react-dom": "catalog:m5kdev",
39
- "uuid": "catalog:m5kdev",
40
- "zod": "catalog:m5kdev"
41
- },
42
- "devDependencies": {
43
- "@m5kdev/config": "catalog:m5kdev",
44
- "@types/express": "catalog:m5kdev",
45
- "@types/node": "catalog:m5kdev",
46
- "@types/react": "catalog:m5kdev",
47
- "@types/react-dom": "catalog:m5kdev",
48
- "drizzle-kit": "catalog:m5kdev",
49
- "tslib": "catalog:m5kdev",
50
- "tsup": "catalog:m5kdev",
51
- "tsx": "catalog:m5kdev",
52
- "typescript": "catalog:m5kdev"
53
- },
54
- "exports": {
55
- "./types": "./src/types.ts"
56
- }
57
- }
1
+ {
2
+ "name": "{{PACKAGE_SCOPE}}/server",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "dev": "tsx watch --env-file=../shared/.env src/index.ts",
8
+ "build": "tsup",
9
+ "start": "node dist/index.js",
10
+ "lint": "biome check .",
11
+ "lint:fix": "biome check . --write",
12
+ "check-types": "tsc --noEmit",
13
+ "drizzle:generate": "drizzle-kit generate",
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
19
+ "drizzle:seed": "tsx --env-file=../shared/.env db.ts seed",
20
+ "drizzle:reset": "tsx --env-file=../shared/.env db.ts reset"
21
+ },
22
+ "dependencies": {
23
+ "{{PACKAGE_SCOPE}}/email": "workspace:*",
24
+ "{{PACKAGE_SCOPE}}/shared": "workspace:*",
25
+ "@m5kdev/backend": "catalog:m5kdev",
26
+ "@m5kdev/commons": "catalog:m5kdev",
27
+ "@mastra/core": "catalog:m5kdev",
28
+ "@mastra/pg": "catalog:m5kdev",
29
+ "@mastra/memory": "catalog:m5kdev",
30
+ "pg": "catalog:m5kdev",
31
+ "@openrouter/ai-sdk-provider": "catalog:m5kdev",
32
+ "@opentelemetry/api": "catalog:m5kdev",
33
+ "@trpc/server": "catalog:m5kdev",
34
+ "better-auth": "catalog:m5kdev",
35
+ "dotenv": "catalog:m5kdev",
36
+ "drizzle-orm": "catalog:m5kdev",
37
+ "drizzle-zod": "catalog:m5kdev",
38
+ "express": "catalog:m5kdev",
39
+ "ioredis": "catalog:m5kdev",
40
+ "neverthrow": "catalog:m5kdev",
41
+ "react": "catalog:m5kdev",
42
+ "react-dom": "catalog:m5kdev",
43
+ "uuid": "catalog:m5kdev",
44
+ "zod": "catalog:m5kdev"
45
+ },
46
+ "devDependencies": {
47
+ "@m5kdev/config": "catalog:m5kdev",
48
+ "@types/express": "catalog:m5kdev",
49
+ "@types/node": "catalog:m5kdev",
50
+ "@types/pg": "catalog:m5kdev",
51
+ "@types/react": "catalog:m5kdev",
52
+ "@types/react-dom": "catalog:m5kdev",
53
+ "drizzle-kit": "catalog:m5kdev",
54
+ "tslib": "catalog:m5kdev",
55
+ "tsup": "catalog:m5kdev",
56
+ "tsx": "catalog:m5kdev",
57
+ "typescript": "catalog:m5kdev"
58
+ },
59
+ "exports": {
60
+ "./types": "./src/types.ts"
61
+ }
62
+ }
@@ -2,6 +2,7 @@
2
2
  import { createBackendApp } from "@m5kdev/backend/app";
3
3
  import { shutdownTelemetry } from "@m5kdev/backend/lib/otel";
4
4
  // m5k:ai:start
5
+ import { defineBackendModule } from "@m5kdev/backend/app";
5
6
  import { AIModule } from "@m5kdev/backend/modules/ai/ai.module";
6
7
  // m5k:ai:end
7
8
  import { createBetterAuth } from "@m5kdev/backend/modules/auth/auth.lib";
@@ -31,7 +32,13 @@ import {
31
32
  import { NOTIFICATION_KINDS } from "{{PACKAGE_SCOPE}}/shared/modules/notification/notification.constants";
32
33
  // m5k:notifications:end
33
34
  // m5k:ai:start
34
- 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";
35
42
  // m5k:ai:end
36
43
  // m5k:workflows:start
37
44
  import { DemoWorkflowModule } from "./modules/demo-workflow/demo-workflow.module";
@@ -44,27 +51,15 @@ import * as schema from "./schema";
44
51
 
45
52
  const appUrl = process.env.VITE_APP_URL ?? "http://localhost:5173";
46
53
  const serverUrl = process.env.VITE_SERVER_URL ?? "http://localhost:8080";
47
- const databaseUrl = process.env.DATABASE_URL ?? "file:./local.db";
48
- const syncUrl = process.env.TURSO_DATABASE_URL;
49
- 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");
50
56
  // m5k:workflows:start
51
57
  const redisUrl = process.env.REDIS_URL ?? "redis://127.0.0.1:6379";
52
58
  // m5k:workflows:end
53
59
  const resendApiKey = process.env.RESEND_API_KEY;
54
60
  const enableWaitlist = process.env.VITE_ENABLE_WAITLIST === "true";
55
61
 
56
- const connection =
57
- syncUrl && authToken
58
- ? {
59
- url: databaseUrl,
60
- syncUrl,
61
- authToken,
62
- syncInterval: 60,
63
- readYourWrites: true,
64
- }
65
- : {
66
- url: databaseUrl,
67
- };
62
+ const connection = { connectionString: databaseUrl };
68
63
 
69
64
  export const builtBackendApp = createBackendApp(
70
65
  {
@@ -129,6 +124,15 @@ export const builtBackendApp = createBackendApp(
129
124
  // m5k:ai:start
130
125
  new AIModule({
131
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(),
132
136
  }),
133
137
  // m5k:ai:end
134
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
@@ -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"
@@ -8,7 +8,7 @@
8
8
  "eyebrow": "Included by Default",
9
9
  "title": "Auth, modules, and one honest feature.",
10
10
  "body": "This starter keeps the stack visible: Better Auth, Drizzle, tRPC, React Router, HeroUI, and a single editorial posts module.",
11
- "local": "Local SQLite",
11
+ "local": "PostgreSQL",
12
12
  "auth": "Better Auth"
13
13
  },
14
14
  "navigation": {
@@ -22,6 +22,11 @@
22
22
  "typescript": "catalog:m5kdev"
23
23
  },
24
24
  "packageManager": "pnpm@10.13.1",
25
+ "pnpm": {
26
+ "overrides": {
27
+ "drizzle-orm>@libsql/client": "-"
28
+ }
29
+ },
25
30
  "engines": {
26
31
  "node": ">=24"
27
32
  }
@@ -6,20 +6,21 @@ catalogs:
6
6
  "@biomejs/biome": 2.5.9
7
7
  "@heroui/react": 3.0.3
8
8
  "@heroui/styles": 3.0.3
9
- "@libsql/client": 0.17.4
10
- "@m5kdev/backend": 0.41.1
11
- "@m5kdev/commons": 0.41.1
12
- "@m5kdev/config": 0.41.1
13
- "@m5kdev/email": 0.41.1
14
- "@m5kdev/expo": 0.41.1
15
- "@m5kdev/frontend": 0.41.1
16
- "@m5kdev/module-clay": 0.41.1
17
- "@m5kdev/module-docx": 0.41.1
18
- "@m5kdev/module-pdf": 0.41.1
19
- "@m5kdev/module-social": 0.41.1
20
- "@m5kdev/module-video": 0.41.1
21
- "@m5kdev/web-ui": 0.41.1
9
+ "@m5kdev/backend": 0.43.0
10
+ "@m5kdev/commons": 0.43.0
11
+ "@m5kdev/config": 0.43.0
12
+ "@m5kdev/email": 0.43.0
13
+ "@m5kdev/expo": 0.43.0
14
+ "@m5kdev/frontend": 0.43.0
15
+ "@m5kdev/module-clay": 0.43.0
16
+ "@m5kdev/module-docx": 0.43.0
17
+ "@m5kdev/module-pdf": 0.43.0
18
+ "@m5kdev/module-social": 0.43.0
19
+ "@m5kdev/module-video": 0.43.0
20
+ "@m5kdev/web-ui": 0.43.0
22
21
  "@mastra/core": 1.51.0
22
+ "@mastra/memory": 1.9.0
23
+ "@mastra/pg": 1.16.0
23
24
  "@openrouter/ai-sdk-provider": 3.0.0
24
25
  "@opentelemetry/api": 1.9.1
25
26
  "@playwright/test": 1.57.0
@@ -31,6 +32,7 @@ catalogs:
31
32
  "@trpc/tanstack-react-query": 11.4.3
32
33
  "@types/express": 5.0.6
33
34
  "@types/node": 20.19.11
35
+ "@types/pg": 8.23.1
34
36
  "@types/react": 19.2.17
35
37
  "@types/react-dom": 19.2.3
36
38
  "@vitejs/plugin-react": 4.2.0
@@ -46,6 +48,7 @@ catalogs:
46
48
  lucide-react: 0.488.0
47
49
  neverthrow: 8.2.0
48
50
  nuqs: 2.4.3
51
+ pg: 8.23.1
49
52
  posthog-js: 1.258.2
50
53
  react: 19.2.3
51
54
  react-dom: 19.2.3
@@ -58,6 +58,7 @@
58
58
  "paths": [
59
59
  "apps/webapp/src/modules/conversation/",
60
60
  "apps/server/src/modules/conversation/",
61
+ "apps/server/mastra.ts",
61
62
  "apps/shared/src/modules/conversation/",
62
63
  "apps/e2e/tests/conversation.spec.ts"
63
64
  ]
@@ -137,6 +138,10 @@
137
138
  "pattern": "**/drizzle/meta/**",
138
139
  "policy": "ignore"
139
140
  },
141
+ {
142
+ "pattern": "**/drizzle-postgresql/**",
143
+ "policy": "ignore"
144
+ },
140
145
  {
141
146
  "pattern": "apps/server/db.ts",
142
147
  "policy": "ensure"
@@ -149,6 +154,10 @@
149
154
  "pattern": "apps/server/drizzle.config.ts",
150
155
  "policy": "ensure"
151
156
  },
157
+ {
158
+ "pattern": "apps/server/src/drizzle.schema.ts",
159
+ "policy": "ensure"
160
+ },
152
161
  {
153
162
  "pattern": "GLOSSARY.md",
154
163
  "policy": "ensure"