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.
- package/package.json +1 -1
- package/templates/minimal-app/.cursor/rules/frontend-component-guide.mdc +95 -95
- package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +103 -103
- package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -80
- package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -79
- package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -45
- package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -60
- package/templates/minimal-app/.cursor/rules/module-prompt-guide.mdc +214 -214
- package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +167 -167
- package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -53
- package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +367 -367
- package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -71
- package/templates/minimal-app/.github/workflows/check.yml.tpl +21 -0
- package/templates/minimal-app/AGENTS.md.tpl +22 -31
- package/templates/minimal-app/CLAUDE.md.tpl +17 -3
- package/templates/minimal-app/GLOSSARY.md.tpl +181 -0
- package/templates/minimal-app/README.md.tpl +14 -6
- package/templates/minimal-app/apps/.gitignore.tpl +13 -13
- package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
- package/templates/minimal-app/apps/e2e/servers.ts.tpl +22 -6
- package/templates/minimal-app/apps/email/package.json.tpl +34 -34
- package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -44
- package/templates/minimal-app/apps/expo/.prettierignore +13 -13
- package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -9
- package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -156
- package/templates/minimal-app/apps/expo/README.md.tpl +44 -44
- package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
- package/templates/minimal-app/apps/expo/package.json.tpl +63 -63
- package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
- package/templates/minimal-app/apps/landing/package.json.tpl +34 -34
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +1 -1
- package/templates/minimal-app/apps/server/drizzle/0000_whole_echo.sql +366 -366
- package/templates/minimal-app/apps/server/drizzle/0002_crazy_lilith.sql +46 -46
- package/templates/minimal-app/apps/server/drizzle/0003_freezing_fantastic_four.sql +3 -3
- package/templates/minimal-app/apps/server/drizzle/0004_slow_ezekiel_stane.sql +98 -98
- package/templates/minimal-app/apps/server/drizzle/0005_wonderful_the_professor.sql +1 -1
- package/templates/minimal-app/apps/server/drizzle/0006_lazy_blue_marvel.sql +158 -158
- package/templates/minimal-app/apps/server/drizzle/0007_abnormal_ultimo.sql +1 -1
- package/templates/minimal-app/apps/server/drizzle/0008_steady_nekra.sql +86 -86
- package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +2624 -2624
- package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +2670 -2670
- package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +2715 -2715
- package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2687 -2687
- package/templates/minimal-app/apps/server/drizzle/meta/0004_snapshot.json.tpl +2497 -2497
- package/templates/minimal-app/apps/server/drizzle/meta/0005_snapshot.json.tpl +2483 -2483
- package/templates/minimal-app/apps/server/drizzle/meta/0006_snapshot.json.tpl +3602 -3602
- package/templates/minimal-app/apps/server/drizzle/meta/0007_snapshot.json.tpl +3619 -3619
- package/templates/minimal-app/apps/server/drizzle/meta/0008_snapshot.json.tpl +3619 -3619
- package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +76 -76
- package/templates/minimal-app/apps/server/drizzle-postgresql/0000_glorious_warhawk.sql +522 -0
- package/templates/minimal-app/apps/server/drizzle-postgresql/meta/0000_snapshot.json.tpl +3555 -0
- package/templates/minimal-app/apps/server/drizzle-postgresql/meta/_journal.json.tpl +13 -0
- package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +1 -1
- package/templates/minimal-app/apps/server/mastra.ts.tpl +42 -0
- package/templates/minimal-app/apps/server/package.json.tpl +9 -4
- package/templates/minimal-app/apps/server/src/app.ts.tpl +21 -16
- package/templates/minimal-app/apps/server/src/drizzle.schema.ts.tpl +2 -0
- package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +48 -0
- package/templates/minimal-app/apps/server/src/modules/conversation/conversation.persistence.ts.tpl +47 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +7 -6
- package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +2 -2
- package/templates/minimal-app/apps/shared/.env.example.tpl +6 -3
- package/templates/minimal-app/apps/shared/.env.production.example.tpl +7 -2
- package/templates/minimal-app/apps/shared/.env.tpl +6 -3
- package/templates/minimal-app/apps/shared/Dockerfile.pgbouncer +6 -0
- package/templates/minimal-app/apps/shared/compose.yaml.tpl +27 -0
- package/templates/minimal-app/apps/shared/fly.toml.tpl +0 -4
- package/templates/minimal-app/apps/shared/package.json.tpl +27 -27
- package/templates/minimal-app/apps/shared/pgbouncer.ini +13 -0
- package/templates/minimal-app/apps/shared/pgbouncer.users +1 -0
- package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -24
- package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
- package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
- package/templates/minimal-app/apps/webapp/public/logo.svg.tpl +6 -6
- package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +1 -1
- package/templates/minimal-app/biome.json.tpl +1 -0
- package/templates/minimal-app/docs/adr/.gitkeep +0 -0
- package/templates/minimal-app/docs/agents/domain.md.tpl +38 -0
- package/templates/minimal-app/docs/agents/issue-tracker.md.tpl +45 -0
- package/templates/minimal-app/docs/agents/triage-labels.md.tpl +15 -0
- package/templates/minimal-app/package.json.tpl +5 -0
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +16 -13
- package/templates/minimal-app/template.manifest.json +21 -0
|
@@ -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": "
|
|
15
|
-
"drizzle:studio": "drizzle-kit studio",
|
|
16
|
-
|
|
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 {
|
|
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
|
|
47
|
-
|
|
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
|
package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl
CHANGED
|
@@ -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
|
},
|
package/templates/minimal-app/apps/server/src/modules/conversation/conversation.persistence.ts.tpl
ADDED
|
@@ -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 {
|
|
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:
|
|
23
|
-
createdAt:
|
|
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:
|
|
27
|
-
deletedAt:
|
|
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 {
|
|
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 =
|
|
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=
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
#
|
|
6
|
-
DATABASE_URL=
|
|
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=
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
|
@@ -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>
|