@percepta/create 3.6.2 → 4.0.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/README.md +37 -6
- package/dist/{git-ops-C2CIjuce.js → git-ops-BD7JNnal.js} +1 -1
- package/dist/{git-ops-C2CIjuce.js.map → git-ops-BD7JNnal.js.map} +1 -1
- package/dist/github-RCIMUq70.js +131 -0
- package/dist/github-RCIMUq70.js.map +1 -0
- package/dist/index.js +68 -125
- package/dist/index.js.map +1 -1
- package/dist/{init-sI9aIrkU.js → init-COp0nGdk.js} +4 -2
- package/dist/{init-sI9aIrkU.js.map → init-COp0nGdk.js.map} +1 -1
- package/dist/manifest-CqIDnbgs.js +58 -0
- package/dist/manifest-CqIDnbgs.js.map +1 -0
- package/dist/register-app-C7ZBpAaZ.js +103 -0
- package/dist/register-app-C7ZBpAaZ.js.map +1 -0
- package/dist/register-os-blueprint-DGjBUZYa.js +90 -0
- package/dist/register-os-blueprint-DGjBUZYa.js.map +1 -0
- package/dist/{status-CKe4aKso.js → status-BXYaQ4a2.js} +3 -3
- package/dist/{status-CKe4aKso.js.map → status-BXYaQ4a2.js.map} +1 -1
- package/dist/{sync-D1vkoofl.js → sync-BayU4w1j.js} +3 -3
- package/dist/{sync-D1vkoofl.js.map → sync-BayU4w1j.js.map} +1 -1
- package/dist/template-versions-CEIP9vhl.js +35 -0
- package/dist/template-versions-CEIP9vhl.js.map +1 -0
- package/dist/{upstream-gUHLWSR1.js → upstream-CZEzLrS4.js} +3 -3
- package/dist/{upstream-gUHLWSR1.js.map → upstream-CZEzLrS4.js.map} +1 -1
- package/dist/validate-dssldJAj.js +14 -0
- package/dist/validate-dssldJAj.js.map +1 -0
- package/package.json +1 -1
- package/template-versions.json +2 -2
- package/templates/infra/os.blueprint.yaml.template +138 -0
- package/templates/library/README.md +5 -2
- package/templates/library/gitignore.template +1 -0
- package/templates/library/package.json.template +17 -13
- package/templates/library/src/index.test.ts +8 -0
- package/templates/library/tsconfig.json +1 -17
- package/templates/library/tsdown.config.ts +3 -0
- package/templates/library/vitest.config.ts +3 -0
- package/templates/monorepo/.dockerignore +1 -0
- package/templates/monorepo/.github/CODEOWNERS +67 -0
- package/templates/monorepo/.github/actions/ci/action.yml +56 -0
- package/templates/monorepo/.github/workflows/build-and-publish.yml +22 -0
- package/templates/monorepo/.github/workflows/pr-build.yml +21 -0
- package/templates/monorepo/.node-version +1 -0
- package/templates/monorepo/README.md +41 -3
- package/templates/monorepo/auth/README.md +6 -3
- package/templates/monorepo/auth/package.json +5 -7
- package/templates/monorepo/auth/src/auth.ts +0 -1
- package/templates/monorepo/auth/src/config/database.ts +1 -1
- package/templates/monorepo/auth/tsconfig.json +1 -10
- package/templates/{webapp → monorepo}/docker-compose.yml +2 -2
- package/templates/monorepo/gitignore.template +1 -0
- package/templates/monorepo/oxfmt.config.ts.template +3 -0
- package/templates/monorepo/oxlint.config.ts.template +3 -0
- package/templates/monorepo/package.json.template +22 -11
- package/templates/monorepo/scripts/setup-local-databases.mjs +183 -0
- package/templates/monorepo/turbo.json +20 -0
- package/templates/webapp/.node-version +0 -1
- package/templates/webapp/AGENTS.md +33 -35
- package/templates/webapp/README.md +34 -38
- package/templates/webapp/agent-skills/database.md +21 -21
- package/templates/webapp/agent-skills/langfuse.md +7 -7
- package/templates/webapp/agent-skills/llm.md +4 -2
- package/templates/webapp/agent-skills/oneshot.md +7 -6
- package/templates/webapp/agent-skills/ryvn.md +12 -16
- package/templates/webapp/deploy/README.md +10 -51
- package/templates/webapp/drizzle.config.ts +2 -23
- package/templates/webapp/env.example.template +8 -14
- package/templates/webapp/globals.d.ts +1 -0
- package/templates/webapp/oxfmt.config.ts.template +5 -0
- package/templates/webapp/package.json.template +18 -33
- package/templates/webapp/scripts/seed.ts +1 -1
- package/templates/webapp/scripts/start.sh +12 -16
- package/templates/webapp/src/app/global-error.tsx +1 -1
- package/templates/webapp/src/config/getEnvConfig.ts +4 -10
- package/templates/webapp/src/config/isDev.ts +0 -2
- package/templates/webapp/src/drizzle/db.ts +6 -21
- package/templates/webapp/src/lib/auth-client.ts +6 -3
- package/templates/webapp/src/startup-checks.ts +28 -7
- package/templates/webapp/tsconfig.json +1 -12
- package/templates/webapp/vitest.config.ts +3 -7
- package/templates/library/eslint.config.js +0 -10
- package/templates/monorepo/auth/scripts/setup-database.ts +0 -11
- package/templates/monorepo/eslint.config.js +0 -10
- package/templates/monorepo/tsconfig.json +0 -16
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +0 -92
- package/templates/webapp/.github/workflows/ci.yml +0 -149
- package/templates/webapp/.prettierrc.mjs +0 -5
- package/templates/webapp/agent-skills/deploy.md +0 -92
- package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +0 -10
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +0 -11
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +0 -154
- package/templates/webapp/eslint.config.mjs +0 -100
- package/templates/webapp/npmrc.template +0 -4
- package/templates/webapp/terraform/README.md +0 -147
- package/templates/webapp/terraform/deploy.sh +0 -97
- package/templates/webapp/terraform/main.tf +0 -101
- package/templates/webapp/terraform/modules/cloudtrail/main.tf +0 -27
- package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +0 -10
- package/templates/webapp/terraform/modules/cloudtrail/variables.tf +0 -15
- package/templates/webapp/terraform/modules/networking/main.tf +0 -118
- package/templates/webapp/terraform/modules/networking/outputs.tf +0 -38
- package/templates/webapp/terraform/modules/networking/variables.tf +0 -24
- package/templates/webapp/terraform/modules/rds/main.tf +0 -227
- package/templates/webapp/terraform/modules/rds/outputs.tf +0 -73
- package/templates/webapp/terraform/modules/rds/variables.tf +0 -61
- package/templates/webapp/terraform/modules/s3-logging/main.tf +0 -148
- package/templates/webapp/terraform/modules/s3-logging/outputs.tf +0 -10
- package/templates/webapp/terraform/modules/s3-logging/variables.tf +0 -16
- package/templates/webapp/terraform/modules/secrets/main.tf +0 -39
- package/templates/webapp/terraform/modules/secrets/outputs.tf +0 -9
- package/templates/webapp/terraform/modules/secrets/variables.tf +0 -51
- package/templates/webapp/terraform/outputs.tf +0 -102
- package/templates/webapp/terraform/providers.tf +0 -32
- package/templates/webapp/terraform/schema/main.tf +0 -4
- package/templates/webapp/terraform/schema/outputs.tf +0 -9
- package/templates/webapp/terraform/schema/variables.tf +0 -19
- package/templates/webapp/terraform/schema/versions.tf +0 -38
- package/templates/webapp/terraform/terraform.tfvars.example +0 -65
- package/templates/webapp/terraform/variables.tf +0 -129
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const LOCAL_POSTGRES_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
9
|
+
const LOCAL_POSTGRES_PORT = "5434";
|
|
10
|
+
const POSTGRES_SERVICE = "postgres";
|
|
11
|
+
const POSTGRES_USER = "postgres";
|
|
12
|
+
const ROOT_DIR = path.resolve(
|
|
13
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
14
|
+
"..",
|
|
15
|
+
);
|
|
16
|
+
const PACKAGES_DIR = path.join(ROOT_DIR, "packages");
|
|
17
|
+
|
|
18
|
+
const databases = new Set(["auth"]);
|
|
19
|
+
|
|
20
|
+
for (const packageDir of listPackageDirs()) {
|
|
21
|
+
const database = readPackageDatabaseName(packageDir);
|
|
22
|
+
if (database != null) {
|
|
23
|
+
databases.add(database);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const database of [...databases].sort()) {
|
|
28
|
+
ensureDatabase(database);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listPackageDirs() {
|
|
32
|
+
if (!existsSync(PACKAGES_DIR)) return [];
|
|
33
|
+
|
|
34
|
+
return readdirSync(PACKAGES_DIR, { withFileTypes: true })
|
|
35
|
+
.filter((entry) => entry.isDirectory())
|
|
36
|
+
.map((entry) => packageDirFor(entry.name));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function packageDirFor(packageName) {
|
|
40
|
+
if (
|
|
41
|
+
packageName === "." ||
|
|
42
|
+
packageName === ".." ||
|
|
43
|
+
packageName.includes("/") ||
|
|
44
|
+
packageName.includes("\\")
|
|
45
|
+
) {
|
|
46
|
+
throw new Error(`Unexpected package directory name: ${packageName}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return path.join(PACKAGES_DIR, packageName);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readPackageDatabaseName(packageDir) {
|
|
53
|
+
const env = readPackageEnvFile(packageDir);
|
|
54
|
+
const databaseUrl = env.DATABASE_URL;
|
|
55
|
+
if (databaseUrl == null || databaseUrl.length === 0) return null;
|
|
56
|
+
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(databaseUrl);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Invalid DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local must use postgres or postgresql.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const port = url.port || "5432";
|
|
73
|
+
if (!LOCAL_POSTGRES_HOSTS.has(url.hostname) || port !== LOCAL_POSTGRES_PORT) {
|
|
74
|
+
console.log(
|
|
75
|
+
`Skipping non-local app database for ${path.relative(ROOT_DIR, packageDir)}.`,
|
|
76
|
+
);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const database = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
|
|
81
|
+
if (database.length === 0) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local must include a database name.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return database;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readPackageEnvFile(packageDir) {
|
|
91
|
+
const safePackageDir = assertPackageDir(packageDir);
|
|
92
|
+
const envPath = path.join(safePackageDir, ".env.local");
|
|
93
|
+
if (!existsSync(envPath)) return {};
|
|
94
|
+
|
|
95
|
+
const env = {};
|
|
96
|
+
const content = readFileSync(envPath, "utf8");
|
|
97
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
98
|
+
const line = rawLine.trim();
|
|
99
|
+
if (!line || line.startsWith("#")) continue;
|
|
100
|
+
|
|
101
|
+
const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
102
|
+
const separatorIndex = normalized.indexOf("=");
|
|
103
|
+
if (separatorIndex === -1) continue;
|
|
104
|
+
|
|
105
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
106
|
+
const rawValue = normalized.slice(separatorIndex + 1).trim();
|
|
107
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
108
|
+
|
|
109
|
+
env[key] = unquote(rawValue);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return env;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertPackageDir(packageDir) {
|
|
116
|
+
const resolvedPackageDir = path.resolve(packageDir);
|
|
117
|
+
const relative = path.relative(PACKAGES_DIR, resolvedPackageDir);
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
relative.length === 0 ||
|
|
121
|
+
relative.startsWith("..") ||
|
|
122
|
+
path.isAbsolute(relative) ||
|
|
123
|
+
relative.includes(path.sep)
|
|
124
|
+
) {
|
|
125
|
+
throw new Error(`Unexpected package directory: ${packageDir}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return resolvedPackageDir;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unquote(value) {
|
|
132
|
+
if (
|
|
133
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
134
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
135
|
+
) {
|
|
136
|
+
return value.slice(1, -1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ensureDatabase(database) {
|
|
143
|
+
const exists = execDockerCompose([
|
|
144
|
+
"exec",
|
|
145
|
+
"-T",
|
|
146
|
+
POSTGRES_SERVICE,
|
|
147
|
+
"psql",
|
|
148
|
+
"-U",
|
|
149
|
+
POSTGRES_USER,
|
|
150
|
+
"-d",
|
|
151
|
+
"postgres",
|
|
152
|
+
"-tAc",
|
|
153
|
+
`SELECT 1 FROM pg_database WHERE datname = ${quotePgLiteral(database)}`,
|
|
154
|
+
]).trim();
|
|
155
|
+
|
|
156
|
+
if (exists === "1") {
|
|
157
|
+
console.log(`Database ${database} already exists.`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
execDockerCompose([
|
|
162
|
+
"exec",
|
|
163
|
+
"-T",
|
|
164
|
+
POSTGRES_SERVICE,
|
|
165
|
+
"createdb",
|
|
166
|
+
"-U",
|
|
167
|
+
POSTGRES_USER,
|
|
168
|
+
database,
|
|
169
|
+
]);
|
|
170
|
+
console.log(`Database ${database} created.`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function execDockerCompose(args) {
|
|
174
|
+
return execFileSync("docker", ["compose", ...args], {
|
|
175
|
+
cwd: ROOT_DIR,
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function quotePgLiteral(value) {
|
|
182
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
183
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turborepo.com/schema.json",
|
|
3
|
+
"ui": "tui",
|
|
4
|
+
"globalDependencies": ["pnpm-workspace.yaml"],
|
|
5
|
+
"tasks": {
|
|
6
|
+
"build": {
|
|
7
|
+
"dependsOn": ["^build"],
|
|
8
|
+
"outputs": ["dist/**", "**/.next/**", "!**/.next/cache/**"]
|
|
9
|
+
},
|
|
10
|
+
"typecheck": {
|
|
11
|
+
"dependsOn": ["^build"]
|
|
12
|
+
},
|
|
13
|
+
"test": {
|
|
14
|
+
"dependsOn": ["^build"]
|
|
15
|
+
},
|
|
16
|
+
"clean": {
|
|
17
|
+
"cache": false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
# Webapp Template
|
|
2
2
|
|
|
3
|
-
Next.js
|
|
3
|
+
Next.js 16 full-stack application scaffolded from the Mosaic webapp template via `@percepta/create`. Uses React 19, TypeScript, Tailwind CSS v4, tRPC, Drizzle ORM, Better Auth, SpiceDB access control, and Inngest.
|
|
4
4
|
|
|
5
5
|
## Build & Dev Commands
|
|
6
6
|
|
|
7
7
|
- `pnpm dev` — start dev server with Turbopack
|
|
8
8
|
- `pnpm build` — production build
|
|
9
|
-
- `pnpm
|
|
9
|
+
- `pnpm typecheck` — type-check with `tsc`
|
|
10
10
|
- `pnpm test` — run Vitest tests
|
|
11
|
-
-
|
|
11
|
+
- Linting and formatting run from the monorepo root: `pnpm lint` (oxlint) and `pnpm format` (oxfmt)
|
|
12
12
|
- `pnpm access:validate` — validate access schema and manifest
|
|
13
13
|
- `pnpm access:apply-local` — apply merged customer access schema to local SpiceDB
|
|
14
|
-
- `pnpm auth:db:
|
|
14
|
+
- `pnpm auth:db:migrate` — run shared customer auth database migrations
|
|
15
15
|
- `pnpm inngest:dev` — start local Inngest dev server when working on background jobs
|
|
16
16
|
- `pnpm db:generate` — generate Drizzle migrations
|
|
17
17
|
- `pnpm db:migrate` — apply migrations
|
|
18
|
-
- `pnpm db:
|
|
19
|
-
- `pnpm db:studio` — setup/migrate the database, then open Drizzle Studio
|
|
18
|
+
- `pnpm db:studio` — run migrations, then open Drizzle Studio
|
|
20
19
|
- `pnpm db:seed` — seed shared-auth dev users and local grants for customer admin, app admin, app user, and app non-user personas (all use password)
|
|
21
20
|
|
|
22
21
|
**Package manager**: Always use `pnpm`, never `npm` or `yarn`.
|
|
23
22
|
|
|
24
|
-
Local
|
|
23
|
+
Local PostgreSQL and SpiceDB are owned by the monorepo root. Use `pnpm run setup` from this package or the root to start services, create local databases, run migrations, and seed users/grants. Run Inngest locally when a workflow needs it. Do not run a local LGTM/Langfuse stack unless you are specifically debugging telemetry; those are wired by the Ryvn environment for deploys.
|
|
25
24
|
If local LLM calls are needed, `pnpm dev` loads shared provider keys from `~/.config/percepta/create.env` when it exists.
|
|
26
25
|
|
|
27
26
|
## Code Style
|
|
28
27
|
|
|
29
28
|
- Double quotes for strings
|
|
30
|
-
- `
|
|
31
|
-
- Logger messages
|
|
32
|
-
-
|
|
29
|
+
- Prefer `@percepta/logger` over `console` (oxlint flags `console` as a warning, see below)
|
|
30
|
+
- Logger messages should be plain string literals, not variables or templates
|
|
31
|
+
- Read env via `getEnvConfig()` from `src/config/` rather than `process.env` directly (a project convention)
|
|
33
32
|
- Use `@percepta/design` components before writing custom UI
|
|
34
33
|
- For loading/error states, use local UI or add `@percepta/components` if you want `AsyncContent`
|
|
35
34
|
- Tailwind CSS for all styling; icons from `lucide-react`
|
|
@@ -58,13 +57,9 @@ src/ # Application source
|
|
|
58
57
|
│ └── observability/ # OpenTelemetry setup
|
|
59
58
|
└── utils/ # Helpers (cn, pathEncryption, etc.)
|
|
60
59
|
|
|
61
|
-
deploy/ #
|
|
60
|
+
deploy/ # Optional release metadata
|
|
62
61
|
└── ryvn/
|
|
63
|
-
|
|
64
|
-
├── __APP_NAME__-terraform.service.yaml # Ryvn schema service
|
|
65
|
-
└── environments/<env>/installations/
|
|
66
|
-
├── __APP_NAME__.env.<env>.serviceinstallation.yaml
|
|
67
|
-
└── __APP_NAME__-terraform.env.<env>.serviceinstallation.yaml
|
|
62
|
+
└── __APP_NAME__.service.yaml # Ryvn web service
|
|
68
63
|
```
|
|
69
64
|
|
|
70
65
|
## @percepta Packages
|
|
@@ -109,22 +104,26 @@ Also includes composite components: `ButtonWithDropdown`, `Combobox`, `IconButto
|
|
|
109
104
|
|
|
110
105
|
### @percepta/build — Shared Build Configs
|
|
111
106
|
|
|
112
|
-
Provides centralized formatter, TypeScript, bundler, and Vitest configuration.
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
// eslint.config.mjs
|
|
116
|
-
import eslint from "@eslint/js";
|
|
117
|
-
import tseslint from "typescript-eslint";
|
|
118
|
-
|
|
119
|
-
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended);
|
|
120
|
-
```
|
|
107
|
+
Provides centralized linter (oxlint), formatter (oxfmt), TypeScript, bundler (tsdown), and Vitest configuration.
|
|
121
108
|
|
|
122
109
|
```json
|
|
123
110
|
// tsconfig.json
|
|
124
111
|
{ "extends": "@percepta/build/tsconfig/web" }
|
|
125
112
|
```
|
|
126
113
|
|
|
127
|
-
|
|
114
|
+
```ts
|
|
115
|
+
// oxfmt.config.ts
|
|
116
|
+
import { defineOxfmtReactConfig } from "@percepta/build/oxfmt";
|
|
117
|
+
export default defineOxfmtReactConfig({ stylesheet: "./src/styles/globals.css" });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// vitest.config.ts
|
|
122
|
+
import { createVitestConfig } from "@percepta/build/vitest";
|
|
123
|
+
export default createVitestConfig({ additionalSetupFiles: ["./vitest.setup.ts"] });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Linting is configured once at the monorepo root via `@percepta/build/oxlint`.
|
|
128
127
|
|
|
129
128
|
### @percepta/components — Optional React Utilities
|
|
130
129
|
|
|
@@ -200,8 +199,7 @@ Detailed how-to guides for each major stack component. Read the relevant guide w
|
|
|
200
199
|
| LLM Observability (Langfuse) | [agent-skills/langfuse.md](agent-skills/langfuse.md) | App uses LLMs and needs trace/eval monitoring |
|
|
201
200
|
| Database (Drizzle) | [agent-skills/database.md](agent-skills/database.md) | Adding tables, writing migrations, querying data |
|
|
202
201
|
| Access Control (SpiceDB) | [agent-skills/access-control.md](agent-skills/access-control.md) | Adding Zed policy, app roles, permissioned resources, or group sync |
|
|
203
|
-
| Deployment (Ryvn) | [agent-skills/ryvn.md](agent-skills/ryvn.md) | Ryvn
|
|
204
|
-
| Deploy to percepta-test | [agent-skills/deploy.md](agent-skills/deploy.md) | Step-by-step deploy using the pre-scaffolded `deploy/ryvn/` IaC files |
|
|
202
|
+
| Deployment (Ryvn) | [agent-skills/ryvn.md](agent-skills/ryvn.md) | Ryvn service release notes; environment installs live in infra |
|
|
205
203
|
| Build App (Oneshot) | [agent-skills/oneshot.md](agent-skills/oneshot.md) | Building a complete app from requirements end-to-end |
|
|
206
204
|
|
|
207
205
|
## Key Patterns
|
|
@@ -221,14 +219,14 @@ Client-side usage via `src/lib/trpc.ts`.
|
|
|
221
219
|
|
|
222
220
|
### Authentication
|
|
223
221
|
|
|
224
|
-
Better Auth is configured in the customer monorepo's shared `@__REPO_NAME__/auth` package. The app imports it through `src/lib/auth/` for local session validation.
|
|
222
|
+
Better Auth is configured in the customer monorepo's shared `@__REPO_NAME__/auth` package. The app imports it through `src/lib/auth/` for local session validation. `DATABASE_URL` is this app's database only; deployed auth should use `AUTH_DATABASE_URL` from the monorepo auth Secret.
|
|
225
223
|
|
|
226
224
|
- **Server-side**: `auth.api.getSession({ headers: await headers() })` — get session in server components or tRPC context
|
|
227
225
|
- **Client-side**: `authClient.useSession()` — React hook from `src/lib/auth-client.ts`
|
|
228
226
|
- **Sign in**: `authClient.signIn.email({ email, password })` — client-side
|
|
229
227
|
- **Sign out**: `authClient.signOut()` — client-side
|
|
230
228
|
- **API route**: `src/app/api/auth/[...all]/route.ts` — Better Auth handler
|
|
231
|
-
- **Env vars**: `BETTER_AUTH_SECRET` (required), `BETTER_AUTH_URL` (defaults to `http://localhost:3000`)
|
|
229
|
+
- **Env vars**: `BETTER_AUTH_SECRET` (required), `BETTER_AUTH_URL` (defaults to `http://localhost:3000`), `AUTH_DATABASE_URL` for deployed shared auth DB wiring
|
|
232
230
|
|
|
233
231
|
### Background Jobs
|
|
234
232
|
|
|
@@ -248,11 +246,11 @@ OpenTelemetry initialized in `src/instrumentation.ts`. Server traces and metrics
|
|
|
248
246
|
|
|
249
247
|
## Deployment
|
|
250
248
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
Blueberry does not generate environment-specific deployments. Customer and
|
|
250
|
+
stack-specific infrastructure, app database registration, runtime services,
|
|
251
|
+
ingress, secrets, and service installations belong in the infra repo. The
|
|
252
|
+
release CI/CD workflow is included under `.github/workflows/` for stacks that
|
|
253
|
+
use Ryvn releases.
|
|
256
254
|
|
|
257
255
|
## Template Sync
|
|
258
256
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# __APP_TITLE__
|
|
2
2
|
|
|
3
|
-
A production-ready Next.js application with authentication, database, logging, background jobs
|
|
3
|
+
A production-ready Next.js application with authentication, database, logging, and background jobs.
|
|
4
4
|
|
|
5
5
|
Design theme: `__MOSAIC_DESIGN_THEME__`
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **Next.js
|
|
9
|
+
- **Next.js 16** with App Router
|
|
10
10
|
- **Authentication** via Better Auth with email/password credentials
|
|
11
11
|
- **Database** with PostgreSQL, Drizzle ORM, and migrations
|
|
12
12
|
- **Access Control** with SpiceDB schema authoring and manifest validation
|
|
@@ -14,7 +14,7 @@ Design theme: `__MOSAIC_DESIGN_THEME__`
|
|
|
14
14
|
- **Background Jobs** with Inngest
|
|
15
15
|
- **LLM Calls** with a provider-backed `LLMService`
|
|
16
16
|
- **Observability** with OpenTelemetry, LGTM-compatible traces/metrics/logs, and Langfuse integration
|
|
17
|
-
- **
|
|
17
|
+
- **Deployment-ready** Dockerfile and environment-neutral service metadata
|
|
18
18
|
- **Type Safety** with TypeScript and Zod schemas
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
@@ -29,9 +29,9 @@ Copy `.env.example` to `.env.local` and configure any app-specific environment v
|
|
|
29
29
|
pnpm run setup
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
This
|
|
33
|
-
|
|
34
|
-
users.
|
|
32
|
+
This delegates to the monorepo root. The root setup starts local PostgreSQL and
|
|
33
|
+
SpiceDB, creates the shared auth database and this app's database, applies the
|
|
34
|
+
local access schema, runs migrations, and seeds dev users.
|
|
35
35
|
|
|
36
36
|
### 3. Start Inngest When Using Background Jobs
|
|
37
37
|
|
|
@@ -49,7 +49,7 @@ pnpm dev
|
|
|
49
49
|
|
|
50
50
|
Open [http://localhost:3000](http://localhost:3000) to see your app.
|
|
51
51
|
|
|
52
|
-
OpenTelemetry, Faro, and Langfuse are optional in local development. Leave their env vars empty unless you are actively debugging telemetry
|
|
52
|
+
OpenTelemetry, Faro, and Langfuse are optional in local development. Leave their env vars empty unless you are actively debugging telemetry. Remote deployments should provide telemetry and Langfuse values through the target platform. General HTTP and database spans go to the OTEL/LGTM pipeline; Langfuse receives only AI SDK spans by default.
|
|
53
53
|
|
|
54
54
|
If you need local LLM calls, set provider keys once in your shell profile or in `~/.config/percepta/create.env`:
|
|
55
55
|
|
|
@@ -87,18 +87,14 @@ src/
|
|
|
87
87
|
| `pnpm dev` | Start development server with Turbopack |
|
|
88
88
|
| `pnpm build` | Build for production |
|
|
89
89
|
| `pnpm start` | Start production server |
|
|
90
|
-
| `pnpm
|
|
91
|
-
| `pnpm docker:up` | Start PostgreSQL and SpiceDB containers and wait until they are healthy |
|
|
92
|
-
| `pnpm docker:down` | Stop PostgreSQL and SpiceDB containers |
|
|
90
|
+
| `pnpm typecheck` | Type-check with `tsc` |
|
|
93
91
|
| `pnpm access:validate` | Validate the access manifest and schema |
|
|
94
92
|
| `pnpm access:apply-local` | Apply the merged customer access schema to local SpiceDB |
|
|
95
|
-
| `pnpm auth:db:
|
|
93
|
+
| `pnpm auth:db:migrate` | Run migrations for the shared customer auth database |
|
|
96
94
|
| `pnpm inngest:dev` | Start the local Inngest dev server for this app |
|
|
97
95
|
| `pnpm db:generate` | Generate Drizzle migrations |
|
|
98
96
|
| `pnpm db:migrate` | Run database migrations |
|
|
99
|
-
| `pnpm db:
|
|
100
|
-
| `pnpm db:setup-and-migrate` | Setup and migrate database |
|
|
101
|
-
| `pnpm db:studio` | Setup/migrate the database, then open Drizzle Studio |
|
|
97
|
+
| `pnpm db:studio` | Run migrations, then open Drizzle Studio |
|
|
102
98
|
| `pnpm db:seed` | Seed default shared-auth dev users and local access grants |
|
|
103
99
|
| `pnpm test:e2e:install` | Install the Chromium browser used by Playwright |
|
|
104
100
|
| `pnpm test:e2e` | Run Playwright e2e tests after local setup |
|
|
@@ -153,15 +149,19 @@ logger.error(
|
|
|
153
149
|
|
|
154
150
|
## Authentication
|
|
155
151
|
|
|
156
|
-
This app consumes the customer monorepo's shared [Better Auth](https://better-auth.com) package, `@__REPO_NAME__/auth`. The app still serves local development auth routes, but the users, sessions, accounts, groups, and group memberships live in the shared customer auth database.
|
|
152
|
+
This app consumes the customer monorepo's shared [Better Auth](https://better-auth.com) package, `@__REPO_NAME__/auth`. The app still serves local development auth routes, but the users, sessions, accounts, groups, and group memberships live in the shared customer auth database. Deployed apps should receive that shared database through `AUTH_DATABASE_URL` from the monorepo auth Secret; `DATABASE_URL` is reserved for this app's own database.
|
|
157
153
|
|
|
158
|
-
Required environment variables:
|
|
154
|
+
Required auth environment variables:
|
|
159
155
|
|
|
160
156
|
```bash
|
|
161
157
|
BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
|
|
162
158
|
BETTER_AUTH_URL=http://localhost:3000
|
|
163
159
|
```
|
|
164
160
|
|
|
161
|
+
Remote deployments should also set `AUTH_DATABASE_URL` from the shared auth
|
|
162
|
+
database Secret. Local development can omit it and use the root-created local
|
|
163
|
+
`auth` database.
|
|
164
|
+
|
|
165
165
|
To create dev users:
|
|
166
166
|
```bash
|
|
167
167
|
pnpm db:seed
|
|
@@ -184,17 +184,17 @@ App permissions are authored in `src/access/schema.zed`; `src/access/access.mani
|
|
|
184
184
|
| `NODE_ENV` | Environment mode | `development` |
|
|
185
185
|
| `APP_BASE_URL` | Base URL for the app | - |
|
|
186
186
|
|
|
187
|
-
### Database
|
|
187
|
+
### App Database
|
|
188
188
|
|
|
189
189
|
| Variable | Description | Default |
|
|
190
190
|
|----------|-------------|---------|
|
|
191
|
-
| `
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
|
196
|
-
|
|
197
|
-
| `
|
|
191
|
+
| `DATABASE_URL` | App PostgreSQL connection URL | `postgresql://postgres:postgres@localhost:5434/__DB_NAME__` |
|
|
192
|
+
|
|
193
|
+
### Shared Auth Database
|
|
194
|
+
|
|
195
|
+
| Variable | Description | Default |
|
|
196
|
+
|----------|-------------|---------|
|
|
197
|
+
| `AUTH_DATABASE_URL` | Shared auth database URL from the monorepo auth Secret | - |
|
|
198
198
|
|
|
199
199
|
### Security
|
|
200
200
|
|
|
@@ -231,13 +231,13 @@ node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
|
|
|
231
231
|
| `LANGFUSE_PUBLIC_KEY` | Langfuse public key |
|
|
232
232
|
| `LANGFUSE_SECRET_KEY` | Langfuse secret key |
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
Set these values through the target deployment platform when Langfuse is enabled.
|
|
235
235
|
|
|
236
236
|
### LLM Providers
|
|
237
237
|
|
|
238
238
|
| Variable | Description |
|
|
239
239
|
|----------|-------------|
|
|
240
|
-
| `ANTHROPIC_API_KEY` | Anthropic API key
|
|
240
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key |
|
|
241
241
|
| `OPENAI_API_KEY` | OpenAI API key for local or non-demo deployments |
|
|
242
242
|
| `LLM_PROVIDER` | Optional provider override: `anthropic` or `openai` |
|
|
243
243
|
| `LLM_MODEL` | Optional model override |
|
|
@@ -254,7 +254,7 @@ const result = await LLMService.create().generateText({
|
|
|
254
254
|
});
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
-
For local development, `pnpm dev` also loads `~/.config/percepta/create.env` when that file exists.
|
|
257
|
+
For local development, `pnpm dev` also loads `~/.config/percepta/create.env` when that file exists. Remote deployments do not use that local file; they should receive provider keys from the target platform.
|
|
258
258
|
|
|
259
259
|
### OpenTelemetry / LGTM
|
|
260
260
|
|
|
@@ -270,7 +270,7 @@ For local development, `pnpm dev` also loads `~/.config/percepta/create.env` whe
|
|
|
270
270
|
| `OTEL_METRICS_EXPORTER` | Set to `otlp` to export server metrics |
|
|
271
271
|
| `OTEL_METRIC_EXPORT_INTERVAL` | Metrics export interval in milliseconds |
|
|
272
272
|
|
|
273
|
-
|
|
273
|
+
Configure these values through the target deployment platform. Application logs are written to stdout so the platform collector can collect them.
|
|
274
274
|
|
|
275
275
|
## Local AWS Development
|
|
276
276
|
|
|
@@ -284,17 +284,13 @@ This application uses the default AWS SDK credential provider chain:
|
|
|
284
284
|
|
|
285
285
|
2. **AWS Credentials File**: Run `aws configure` or `aws sso login`
|
|
286
286
|
|
|
287
|
-
## Infrastructure
|
|
288
|
-
|
|
289
|
-
The `terraform/` directory contains modules for AWS infrastructure:
|
|
290
|
-
|
|
291
|
-
- **RDS**: PostgreSQL database
|
|
292
|
-
- **S3**: File storage with logging
|
|
293
|
-
- **Networking**: VPC endpoints
|
|
294
|
-
- **Secrets**: AWS Secrets Manager
|
|
295
|
-
- **IAM**: Roles and policies
|
|
287
|
+
## Infrastructure
|
|
296
288
|
|
|
297
|
-
|
|
289
|
+
The app template does not own cloud infrastructure. The infra repo owns base
|
|
290
|
+
environment infrastructure, customer monorepo infrastructure, app database
|
|
291
|
+
registration, observability, and shared services. App deployments expect the
|
|
292
|
+
customer monorepo infrastructure blueprint to create an app-scoped Kubernetes
|
|
293
|
+
Secret named `__APP_NAME__-postgresql`.
|
|
298
294
|
|
|
299
295
|
## Learn More
|
|
300
296
|
|
|
@@ -38,11 +38,9 @@ export * from "./documents";
|
|
|
38
38
|
pnpm db:generate
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
This creates a new SQL migration file and normalizes generated
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
automatically but you should verify it's correct, especially for new foreign
|
|
45
|
-
keys and destructive changes.
|
|
41
|
+
This creates a new SQL migration file and normalizes generated SQL. **Review
|
|
42
|
+
the generated SQL** — Drizzle generates it automatically but you should verify
|
|
43
|
+
it's correct, especially for new foreign keys and destructive changes.
|
|
46
44
|
|
|
47
45
|
### 4. Apply the migration
|
|
48
46
|
|
|
@@ -95,21 +93,25 @@ If `createTransaction` is called while already inside a transaction, it reuses t
|
|
|
95
93
|
|
|
96
94
|
## Running PostgreSQL Locally
|
|
97
95
|
|
|
98
|
-
### Start with
|
|
96
|
+
### Start with root setup
|
|
99
97
|
|
|
100
98
|
```bash
|
|
101
|
-
pnpm
|
|
99
|
+
pnpm run setup
|
|
102
100
|
```
|
|
103
101
|
|
|
104
|
-
This
|
|
102
|
+
This delegates to the monorepo root. Root setup starts PostgreSQL 16 on port
|
|
103
|
+
**5434** and SpiceDB on **50051**, creates the shared auth database plus each
|
|
104
|
+
app database discovered from `packages/*/.env.local`, then runs migrations and
|
|
105
|
+
seeds.
|
|
105
106
|
|
|
106
|
-
###
|
|
107
|
+
### Run migrations
|
|
107
108
|
|
|
108
109
|
```bash
|
|
109
|
-
pnpm db:
|
|
110
|
+
pnpm db:migrate
|
|
110
111
|
```
|
|
111
112
|
|
|
112
|
-
This
|
|
113
|
+
This applies all pending migrations. Database creation is owned by the local
|
|
114
|
+
infrastructure setup, not by the app migration command.
|
|
113
115
|
|
|
114
116
|
### Inspect the database
|
|
115
117
|
|
|
@@ -119,27 +121,25 @@ pnpm db:studio
|
|
|
119
121
|
|
|
120
122
|
Opens Drizzle Studio in the browser — a visual database explorer.
|
|
121
123
|
|
|
122
|
-
### Stop
|
|
124
|
+
### Stop local services
|
|
123
125
|
|
|
124
126
|
```bash
|
|
125
|
-
pnpm docker:down
|
|
127
|
+
pnpm --dir ../.. run docker:down
|
|
126
128
|
```
|
|
127
129
|
|
|
128
130
|
## Environment Variables
|
|
129
131
|
|
|
130
132
|
| Variable | Default | Description |
|
|
131
133
|
|----------|---------|-------------|
|
|
132
|
-
| `
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
| `DATABASE_SCHEMA` | - | Optional Postgres schema/search path |
|
|
138
|
-
| `DATABASE_USE_SSL` | `false` | Enable SSL connections |
|
|
134
|
+
| `DATABASE_URL` | `postgresql://postgres:postgres@localhost:5434/__DB_NAME__` | App PostgreSQL connection URL |
|
|
135
|
+
|
|
136
|
+
Shared auth data belongs to the customer monorepo auth package. Deployed apps
|
|
137
|
+
should receive `AUTH_DATABASE_URL` from the monorepo auth Secret; do not reuse
|
|
138
|
+
`DATABASE_URL` for auth.
|
|
139
139
|
|
|
140
140
|
## Key Concepts
|
|
141
141
|
|
|
142
142
|
- **Schemas are TypeScript, migrations are SQL.** You define tables in TS, then `pnpm db:generate` creates the SQL diff. Never hand-write migration SQL.
|
|
143
143
|
- **DatabaseService is a singleton.** Call `DatabaseService.create()` anywhere — it always returns the same instance with the same connection pool.
|
|
144
144
|
- **Transaction propagation is automatic.** Code inside `createTransaction` gets a transaction; code outside gets the raw connection. `getDatabase()` returns whichever is active.
|
|
145
|
-
- **
|
|
145
|
+
- **Root-owned local infra.** The monorepo root Docker Compose uses port 5434 to avoid conflicting with any system PostgreSQL on 5432.
|
|
@@ -60,11 +60,11 @@ This is a singleton — `create()` returns the same instance every time. It grac
|
|
|
60
60
|
|
|
61
61
|
## Getting Langfuse Keys
|
|
62
62
|
|
|
63
|
-
###
|
|
63
|
+
### Shared Instances
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
If the target stack provides a shared Langfuse instance, store
|
|
66
|
+
`LANGFUSE_PUBLIC_KEY` and sensitive `LANGFUSE_SECRET_KEY` through that
|
|
67
|
+
platform's environment-scoped variable or secret mechanism.
|
|
68
68
|
|
|
69
69
|
### Self-Hosted / Langfuse Cloud
|
|
70
70
|
|
|
@@ -74,17 +74,17 @@ For external projects, you can:
|
|
|
74
74
|
|
|
75
75
|
## Running Locally
|
|
76
76
|
|
|
77
|
-
### Option 1: Use
|
|
77
|
+
### Option 1: Use a Shared Langfuse Instance
|
|
78
78
|
|
|
79
79
|
Set the keys in `.env.local` if you want local traces to go to the shared instance:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
LANGFUSE_BASE_URL=https://langfuse.
|
|
82
|
+
LANGFUSE_BASE_URL=https://langfuse.example.com
|
|
83
83
|
LANGFUSE_PUBLIC_KEY=pk-lf-...
|
|
84
84
|
LANGFUSE_SECRET_KEY=sk-lf-...
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Traces from local dev appear in the shared dashboard under your project.
|
|
87
|
+
Traces from local dev appear in the configured shared dashboard under your project.
|
|
88
88
|
|
|
89
89
|
### Option 2: Run Langfuse locally
|
|
90
90
|
|
|
@@ -40,7 +40,9 @@ The shared `@percepta/ai` provider helper chooses a provider at call time:
|
|
|
40
40
|
|
|
41
41
|
## Deployment
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Provider keys and Langfuse credentials should come from the target deployment
|
|
44
|
+
platform. Do not commit shared provider keys or generated environment-specific
|
|
45
|
+
secret files to the app repo.
|
|
44
46
|
|
|
45
47
|
## Local Development
|
|
46
48
|
|
|
@@ -54,6 +56,6 @@ ANTHROPIC_API_KEY=sk-ant-...
|
|
|
54
56
|
|
|
55
57
|
## Observability
|
|
56
58
|
|
|
57
|
-
`LLMService` enables AI SDK telemetry by default and attaches provider/model metadata to each call. When `LANGFUSE_*` values are configured, the template's OpenTelemetry bootstrap sends LLM spans to Langfuse.
|
|
59
|
+
`LLMService` enables AI SDK telemetry by default and attaches provider/model metadata to each call. When `LANGFUSE_*` values are configured, the template's OpenTelemetry bootstrap sends LLM spans to Langfuse.
|
|
58
60
|
|
|
59
61
|
Use a stable `telemetryFunctionId` for every meaningful LLM operation, such as `extract-invoice-fields` or `draft-member-message`.
|