@percepta/create 3.1.2 → 3.1.4

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 (51) hide show
  1. package/README.md +3 -4
  2. package/dist/{chunk-CG7IJSB4.js → chunk-CO3YWUD6.js} +2 -2
  3. package/dist/{chunk-WMJT7CB5.js → chunk-V5EJIUBJ.js} +5 -2
  4. package/dist/index.js +21 -53
  5. package/dist/{init-XDWSYHYK.js → init-EQZ2TCSJ.js} +2 -2
  6. package/dist/{status-BTHGN6QH.js → status-QW5TQDYY.js} +1 -1
  7. package/dist/{sync-3Q27L7XZ.js → sync-RLBZDOFB.js} +1 -1
  8. package/dist/{upstream-C5KFAHVR.js → upstream-TQFVPMEG.js} +1 -1
  9. package/package.json +1 -1
  10. package/templates/monorepo/.dockerignore +18 -0
  11. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +6 -2
  12. package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +98 -0
  13. package/templates/webapp/AGENTS.md +18 -6
  14. package/templates/webapp/Dockerfile +16 -7
  15. package/templates/webapp/README.md +65 -3
  16. package/templates/webapp/agent-skills/database.md +5 -1
  17. package/templates/webapp/agent-skills/deploy.md +49 -64
  18. package/templates/webapp/agent-skills/inngest.md +17 -12
  19. package/templates/webapp/agent-skills/langfuse.md +15 -14
  20. package/templates/webapp/agent-skills/llm.md +59 -0
  21. package/templates/webapp/agent-skills/oneshot.md +15 -2
  22. package/templates/webapp/agent-skills/ryvn.md +1 -1
  23. package/templates/webapp/deploy/README.md +34 -33
  24. package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +10 -0
  25. package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +2 -2
  26. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +11 -0
  27. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +45 -9
  28. package/templates/webapp/env.example.template +20 -2
  29. package/templates/webapp/eslint.config.mjs +6 -0
  30. package/templates/webapp/next.config.ts +9 -0
  31. package/templates/webapp/package.json.template +8 -4
  32. package/templates/webapp/scripts/deploy-percepta-test.ts +1112 -0
  33. package/templates/webapp/scripts/generate-migrations.ts +28 -0
  34. package/templates/webapp/scripts/migrate.ts +3 -0
  35. package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +5 -3
  36. package/templates/webapp/scripts/with-local-env.ts +75 -0
  37. package/templates/webapp/src/config/getEnvConfig.ts +14 -0
  38. package/templates/webapp/src/drizzle/__tests__/migrationSql.test.ts +24 -0
  39. package/templates/webapp/src/drizzle/migrationSql.ts +8 -0
  40. package/templates/webapp/src/instrumentation.ts +102 -10
  41. package/templates/webapp/src/services/inngest/AppWorkflowService.ts +19 -0
  42. package/templates/webapp/src/services/inngest/__tests__/AppWorkflowService.test.ts +19 -0
  43. package/templates/webapp/src/services/inngest/events/AppEvents.ts +7 -13
  44. package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +1 -3
  45. package/templates/webapp/src/services/llm/LLMService.ts +88 -0
  46. package/templates/webapp/src/services/llm/LlmProviderService.ts +85 -0
  47. package/templates/webapp/terraform/schema/main.tf +4 -0
  48. package/templates/webapp/terraform/schema/outputs.tf +9 -0
  49. package/templates/webapp/terraform/schema/variables.tf +19 -0
  50. package/templates/webapp/terraform/schema/versions.tf +38 -0
  51. package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +0 -28
@@ -9,7 +9,8 @@ A production-ready Next.js application with authentication, database, logging, b
9
9
  - **Database** with PostgreSQL, Drizzle ORM, and migrations
10
10
  - **Logging** with Pino and structured safe/unsafe data separation
11
11
  - **Background Jobs** with Inngest
12
- - **Observability** with OpenTelemetry and Langfuse integration
12
+ - **LLM Calls** with a provider-backed `LLMService`
13
+ - **Observability** with OpenTelemetry, LGTM-compatible traces/metrics/logs, and Langfuse integration
13
14
  - **Infrastructure** with Terraform modules for AWS (RDS, S3, IAM)
14
15
  - **Type Safety** with TypeScript and Zod schemas
15
16
 
@@ -31,7 +32,15 @@ pnpm db:setup-and-migrate
31
32
 
32
33
  Copy `.env.example` to `.env.local` and configure your environment variables.
33
34
 
34
- ### 4. Start Development Server
35
+ ### 4. Start Inngest When Using Background Jobs
36
+
37
+ ```bash
38
+ pnpm inngest:dev
39
+ ```
40
+
41
+ Run this in a separate terminal when the app has Inngest functions or you want the local Inngest dashboard.
42
+
43
+ ### 5. Start Development Server
35
44
 
36
45
  ```bash
37
46
  pnpm dev
@@ -39,6 +48,16 @@ pnpm dev
39
48
 
40
49
  Open [http://localhost:3000](http://localhost:3000) to see your app.
41
50
 
51
+ OpenTelemetry, Faro, and Langfuse are optional in local development. Leave their env vars empty unless you are actively debugging telemetry; production deploys wire server traces, metrics, logs, and shared Langfuse demo credentials into the target Ryvn environment. General HTTP and database spans go to the OTEL/LGTM pipeline; Langfuse receives only AI SDK spans by default.
52
+
53
+ If you need local LLM calls, set provider keys once in your shell profile or in `~/.config/percepta/create.env`:
54
+
55
+ ```bash
56
+ ANTHROPIC_API_KEY=sk-ant-...
57
+ ```
58
+
59
+ `pnpm dev` loads that shared file before starting Next.js, so you do not need to copy the same provider key into every generated app.
60
+
42
61
  ## Project Structure
43
62
 
44
63
  ```
@@ -53,6 +72,7 @@ src/
53
72
  ├── services/ # Business logic services
54
73
  │ ├── inngest/ # Background job definitions
55
74
  │ ├── langfuse/ # LLM observability
75
+ │ ├── llm/ # LLM provider selection and call helpers
56
76
  │ └── logger/ # Structured logging
57
77
  └── utils/ # Utility functions
58
78
  ```
@@ -65,8 +85,9 @@ src/
65
85
  | `pnpm build` | Build for production |
66
86
  | `pnpm start` | Start production server |
67
87
  | `pnpm lint` | Run ESLint |
68
- | `pnpm docker:up` | Start PostgreSQL container |
88
+ | `pnpm docker:up` | Start PostgreSQL container and wait until it is healthy |
69
89
  | `pnpm docker:down` | Stop PostgreSQL container |
90
+ | `pnpm inngest:dev` | Start the local Inngest dev server for this app |
70
91
  | `pnpm db:generate` | Generate Drizzle migrations |
71
92
  | `pnpm db:migrate` | Run database migrations |
72
93
  | `pnpm db:setup` | Create database and user |
@@ -164,6 +185,47 @@ node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
164
185
  | `LANGFUSE_PUBLIC_KEY` | Langfuse public key |
165
186
  | `LANGFUSE_SECRET_KEY` | Langfuse secret key |
166
187
 
188
+ `deploy:percepta-test` sets the shared Langfuse URL and inherits the demo project keys from the `demos-commons` Ryvn variable group.
189
+
190
+ ### LLM Providers
191
+
192
+ | Variable | Description |
193
+ |----------|-------------|
194
+ | `ANTHROPIC_API_KEY` | Anthropic API key. Inherited from `demos-commons` for `percepta-test` deploys |
195
+ | `OPENAI_API_KEY` | OpenAI API key for local or non-demo deployments |
196
+ | `LLM_PROVIDER` | Optional provider override: `anthropic` or `openai` |
197
+ | `LLM_MODEL` | Optional model override |
198
+
199
+ Use `LLMService` for backend model calls:
200
+
201
+ ```typescript
202
+ import { LLMService } from "@/services/llm/LLMService";
203
+
204
+ const result = await LLMService.create().generateText({
205
+ telemetryFunctionId: "summarize-note",
206
+ system: "You summarize notes for internal users.",
207
+ prompt: "Summarize this note...",
208
+ });
209
+ ```
210
+
211
+ For local development, `pnpm dev` also loads `~/.config/percepta/create.env` when that file exists. Production deploys do not use that local file; they inherit provider keys from the target Ryvn environment.
212
+
213
+ ### OpenTelemetry / LGTM
214
+
215
+ | Variable | Description |
216
+ |----------|-------------|
217
+ | `OTEL_SERVICE_NAME` | Service name attached to traces and metrics |
218
+ | `OTEL_RESOURCE_ATTRIBUTES` | Extra resource labels such as deployment environment |
219
+ | `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol, usually `http/protobuf` |
220
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP HTTP collector endpoint |
221
+ | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Optional trace-specific OTLP endpoint |
222
+ | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Optional metric-specific OTLP endpoint |
223
+ | `OTEL_TRACES_EXPORTER` | Set to `otlp` to export server traces |
224
+ | `OTEL_METRICS_EXPORTER` | Set to `otlp` to export server metrics |
225
+ | `OTEL_METRIC_EXPORT_INTERVAL` | Metrics export interval in milliseconds |
226
+
227
+ `deploy:percepta-test` configures these for the existing percepta-test OTEL collector and LGTM stack. Application logs are written to stdout and collected by the platform collector.
228
+
167
229
  ## Local AWS Development
168
230
 
169
231
  This application uses the default AWS SDK credential provider chain:
@@ -38,7 +38,11 @@ export * from "./documents";
38
38
  pnpm db:generate
39
39
  ```
40
40
 
41
- This creates a new SQL migration file. **Review the generated SQL** — Drizzle generates it automatically but you should verify it's correct.
41
+ This creates a new SQL migration file and normalizes generated foreign key
42
+ references so they stay schema-relative when `DATABASE_SCHEMA` is set in
43
+ `percepta-test`. **Review the generated SQL** — Drizzle generates it
44
+ automatically but you should verify it's correct, especially for new foreign
45
+ keys and destructive changes.
42
46
 
43
47
  ### 4. Apply the migration
44
48
 
@@ -1,107 +1,92 @@
1
1
  # Deploying to Percepta Test
2
2
 
3
- This guide deploys __APP_TITLE__ to `https://__APP_NAME__.percepta-test.aitco.dev` using Ryvn. Tell Claude "deploy this app to percepta-test" and it will follow the steps below.
3
+ This guide deploys __APP_TITLE__ to `https://__APP_NAME__.percepta-test.aitco.dev` using Ryvn. Tell Claude "deploy this app to percepta-test" and it should run the direct deploy helper below.
4
4
 
5
- ## What's already scaffolded
5
+ This is the existing-environment deploy motion: `percepta-test` already owns the shared platform services, and this app is wired into them. Fresh-environment platform bootstrap is separate and should use a Ryvn blueprint or environment-specific platform rollout before app deploys run.
6
6
 
7
- When this app was created with `@percepta/create`, the IaC files were generated at `deploy/ryvn/` with all values pre-filled:
7
+ ## What's Already Scaffolded
8
8
 
9
- - `deploy/ryvn/__APP_NAME__.service.yaml` — Ryvn Service definition
10
- - `deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml` — percepta-test installation
11
- - `.github/workflows/__APP_NAME__-ryvn-release.yaml` (at the repo root) release workflow that builds the Docker image and creates a Ryvn release on push to `main`. Path filter scoped to `packages/__APP_NAME__/` so unrelated changes don't trigger builds.
12
-
13
- Per-app values (URLs, k8s service names, database schema) are substituted at create-time. The Better Auth and encryption secrets are written to `deploy/ryvn/percepta-test.secrets.env`, which is ignored by git and intended for Ryvn UI import. Shared platform values (Inngest and OTel endpoints) are baked in as literals because they're stable across percepta-test apps.
9
+ - `deploy/ryvn/__APP_NAME__.service.yaml` — Ryvn server service for the web app.
10
+ - `deploy/ryvn/__APP_NAME__-terraform.service.yaml` — Ryvn Terraform service that creates the app's Postgres schema.
11
+ - `deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml` — web installation.
12
+ - `deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml` — schema installation.
13
+ - `.github/workflows/__APP_NAME__-ryvn-release.yaml` builds the Docker image and creates the web Ryvn release.
14
+ - `.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml` — creates the schema Terraform Ryvn release.
15
+ - `deploy/ryvn/percepta-test.secrets.env` — generated locally and ignored by git; injected into the app installation as Ryvn secrets by the deploy helper.
14
16
 
15
17
  See [`deploy/README.md`](../deploy/README.md) for the file-by-file breakdown.
16
18
 
17
19
  ## Prerequisites
18
20
 
19
- - The app repo is under the `Percepta-Core` GitHub org. If not yet, run `gh repo create Percepta-Core/__APP_NAME__ --private --source=. --push` from the monorepo root.
20
- - The Percepta-Core org has `RYVN_CLIENT_ID`, `RYVN_CLIENT_SECRET`, and `NPM_TOKEN` as org-level GitHub secrets (already in place).
21
- - The `percepta-internal-terraform` installation provides the shared PostgreSQL already deployed on percepta-test.
22
- - `git` and `gh` are installed and authenticated.
23
- - The infra repo (`Percepta-Core/infra`) is checked out locally or can be cloned by the deploy script.
21
+ - `git`, `gh`, and `ryvn` are installed and authenticated.
22
+ - The worktree is clean and committed. The helper pushes the current branch to `main` because GitHub Actions builds from GitHub.
23
+ - The Percepta-Core org has `RYVN_CLIENT_ID`, `RYVN_CLIENT_SECRET`, and `NPM_TOKEN` available as org-level GitHub secrets.
24
+ - These shared platform installations are already deployed and healthy in `percepta-test`: `percepta-internal-terraform`, `inngest-test`, `otel-collector`, `lgtm-stack-helm`, and `langfuse`.
25
+ - The `demos-commons` Ryvn variable group exists in `percepta-test` and provides `LANGFUSE_PUBLIC_KEY` plus sensitive `ANTHROPIC_API_KEY` and `LANGFUSE_SECRET_KEY` for shared demo LLM calls and Langfuse tracing.
24
26
 
25
27
  ## Deploy
26
28
 
27
- ### Step 1: Open the service/schema infra PR
28
-
29
- Run the generated deploy helper from this package directory:
29
+ From this package directory:
30
30
 
31
31
  ```bash
32
- pnpm deploy:percepta-test -- --phase service --yes
32
+ pnpm deploy:percepta-test -- --yes
33
33
  ```
34
34
 
35
- This script:
36
-
37
- - uses `INFRA_REPO` or `--infra <path>` when provided
38
- - otherwise uses a sibling `../infra` checkout, cloning `Percepta-Core/infra` there if needed
39
- - copies the Ryvn service YAML into infra
40
- - appends a `postgresql_schema "__APP_NAME_SNAKE__"` resource to `terraform/percepta-internal/databases.tf`
41
- - opens the first PR against `Percepta-Core/infra`
42
-
43
- Get the PR reviewed and merged.
44
-
45
- ### Step 2: Wait for service/schema import
35
+ The helper:
46
36
 
47
- After the service/schema PR merges, wait for Ryvn GitOps to import the service. If Ryvn creates a `percepta-internal-terraform` task for the database schema, approve/apply it in Ryvn.
37
+ 1. Checks the existing platform installations and shared demo variable group in `percepta-test`.
38
+ 2. Creates `Percepta-Core/__REPO_NAME__` if needed.
39
+ 3. Pushes the current branch to `main`.
40
+ 4. Creates or replaces the Ryvn web and schema services.
41
+ 5. Runs the schema Terraform release workflow.
42
+ 6. Creates or replaces the schema installation and approves the Terraform plan.
43
+ 7. Runs the web release workflow.
44
+ 8. Creates or replaces the web installation.
45
+ 9. Creates or updates app-scoped Ryvn installation secrets for `BETTER_AUTH_SECRET` and `ENCRYPTION_SECRET_KEY` from `deploy/ryvn/percepta-test.secrets.env`. On first install, the helper injects them into the create manifest so the first pod starts with auth configured.
46
+ 10. Waits for Ryvn health and checks `/api/healthz`, `/api/readyz`, and the protected app route.
48
47
 
49
- ### Step 3: Create the first release
50
-
51
- Push to `main` in the app repo (or `gh workflow run "Build & Release __APP_NAME__"`). The release workflow builds the Docker image and creates a Ryvn release. Do this before creating the ServiceInstallation, otherwise GitOps can fail with `ReleaseNotFound`.
52
-
53
- The workflow lives at `.github/workflows/__APP_NAME__-ryvn-release.yaml` (at the repo root, where GitHub Actions picks it up). It only fires on changes under `packages/__APP_NAME__/`, so unrelated edits to other packages in the monorepo won't trigger it.
54
-
55
- ### Step 4: Open the installation infra PR
48
+ The app will be available at **https://__APP_NAME__.percepta-test.aitco.dev**.
56
49
 
57
- After the first release exists, open the installation PR:
50
+ ## Useful Variants
58
51
 
59
52
  ```bash
60
- pnpm deploy:percepta-test -- --phase installation --yes
53
+ pnpm deploy:percepta-test -- --skip-workflows --yes
54
+ pnpm deploy:percepta-test -- --skip-push --yes
55
+ pnpm deploy:percepta-test -- --timeout-minutes 30 --yes
61
56
  ```
62
57
 
63
- The `--` is the pnpm argument delimiter; it passes `--phase` and `--yes` through to the deploy helper script.
64
-
65
- Get the PR reviewed and merged. Ryvn GitOps will import the ServiceInstallation and the Staging channel should deploy the latest release.
58
+ Use `--skip-workflows` when the required Ryvn releases already exist. Use `--skip-push` only when the target ref is already pushed.
66
59
 
67
- ### Step 5: Import Ryvn secrets
68
-
69
- After GitOps imports the installation, import this generated file in the Ryvn UI for the installation secrets:
60
+ The legacy infra-PR path is still available:
70
61
 
71
62
  ```bash
72
- deploy/ryvn/percepta-test.secrets.env
63
+ pnpm deploy:percepta-test:pr -- --phase service --yes
64
+ pnpm deploy:percepta-test:pr -- --phase installation --yes
73
65
  ```
74
66
 
75
- Also set any app-specific secrets the implementation added, such as `OPENAI_API_KEY`.
76
-
77
- ### Step 6: Verify
67
+ ## Verify
78
68
 
79
69
  ```bash
80
70
  ryvn get installation __APP_NAME__ -e percepta-test
81
71
  ryvn logs __APP_NAME__ -e percepta-test
82
72
  curl -s https://__APP_NAME__.percepta-test.aitco.dev/api/healthz
83
73
  curl -s https://__APP_NAME__.percepta-test.aitco.dev/api/readyz
74
+ curl -I https://__APP_NAME__.percepta-test.aitco.dev/
84
75
  ```
85
76
 
86
- The app will be available at **https://__APP_NAME__.percepta-test.aitco.dev**.
77
+ For apps with tRPC routes, also verify at least one endpoint that initializes Better Auth or app services. `healthz` can be green even when app-specific secrets or workflow wiring are wrong.
87
78
 
88
79
  ## Troubleshooting
89
80
 
90
- - **Auth/sign-in routes fail after install** → import `deploy/ryvn/percepta-test.secrets.env` in the Ryvn UI, then let Ryvn update/restart the installation.
81
+ - **Image build fails fetching @percepta packages** → check the Percepta-Core org-level `NPM_TOKEN` secret. Do not add a repo-level token unless the org secret is unavailable.
82
+ - **Ryvn release already exists** → commit a new change or re-run with `--skip-workflows` if the current releases are already present.
83
+ - **Terraform plan needs approval** → the helper approves it when run with `--yes`; without `--yes`, approve the prompt.
84
+ - **Auth/sign-in or tRPC routes fail after install** → verify the `__APP_NAME__` installation has `BETTER_AUTH_SECRET` and `ENCRYPTION_SECRET_KEY` secrets from `deploy/ryvn/percepta-test.secrets.env`, then redeploy `__APP_NAME__` so the pod reloads them.
91
85
  - **Pod crash-looping** → check `ryvn logs`; migration or database connectivity failures are the most common fresh-deploy causes.
92
- - **Database connection refused** → verify `DATABASE_USE_SSL=true` and that `percepta-internal-terraform` is deployed.
93
- - **Database schema missing** → verify the infra PR added `postgresql_schema "__APP_NAME_SNAKE__"` and Ryvn applied `percepta-internal-terraform`.
94
- - **Inngest can't reach the app** → `INNGEST_APP_URL` must use the k8s service name `__APP_NAME__-web-server`. The scaffolded YAML gets this right; if you renamed anything, double-check.
95
- - **Image build fails fetching @percepta packages** → check the `NPM_TOKEN` GitHub org secret. The workflow passes it as a build arg.
86
+ - **Database schema missing** → check `ryvn get installation __APP_NAME__-terraform -e percepta-test`.
87
+ - **Inngest can't reach the app** → `INNGEST_APP_URL` must use the k8s service name `__APP_NAME__-web-server`.
88
+ - **Platform preflight fails** → deploy or repair the missing shared installation first. This helper only wires apps into an existing environment.
89
+ - **No Langfuse traces** → verify the target environment has Langfuse deployed and that the `demos-commons` variable group has `LANGFUSE_PUBLIC_KEY` and sensitive `LANGFUSE_SECRET_KEY`.
90
+ - **LLM calls fail after deploy** → verify `demos-commons` has sensitive `ANTHROPIC_API_KEY` and the installation has `LLM_PROVIDER=anthropic`.
96
91
 
97
92
  For Ryvn CLI operations, use the `/use-ryvn` skill.
98
-
99
- ## Updating shared platform values
100
-
101
- The Inngest and OTel URLs are baked as literals in every webapp's installation YAML. If percepta-test infra ever changes those endpoints, update them with a single grep across the infra repo:
102
-
103
- ```bash
104
- grep -rl "inngest.percepta-test.svc.cluster.local:8288" ryvn/environments/percepta-test/installations/
105
- ```
106
-
107
- A future improvement would be a shared ConfigMap or Ryvn output reference; for now the scaffolded literals are the convention.
@@ -31,13 +31,13 @@ Add the event to the central `AppEvents` registry:
31
31
  import { DocumentProcessedPayload } from "./payloads/DocumentProcessedPayload";
32
32
 
33
33
  export const AppEvents = {
34
- "app/document.processed": z.object({
35
- data: DocumentProcessedPayload.SCHEMA,
36
- }),
34
+ "app/document.processed": DocumentProcessedPayload.SCHEMA,
37
35
  };
38
36
  ```
39
37
 
40
- Event names follow the convention `"app/<entity>.<action>"`.
38
+ Event names follow the convention `"app/<entity>.<action>"`. `AppEvents`
39
+ schemas validate `event.data`, so do not wrap payload schemas in another
40
+ `{ data: ... }` object.
41
41
 
42
42
  ## Adding a New Function
43
43
 
@@ -93,22 +93,27 @@ const functionCollections: InngestFunctionCollection[] = compact([
93
93
  ## Sending Events
94
94
 
95
95
  ```typescript
96
- const inngest = InngestService.create();
97
- await inngest.client.send({
98
- name: "app/document.processed",
99
- data: { documentId: "abc", userId: "user-1", pageCount: 5 },
96
+ await AppWorkflowService.create().sendDocumentProcessed({
97
+ documentId: "abc",
98
+ userId: "user-1",
99
+ pageCount: 5,
100
100
  });
101
101
  ```
102
102
 
103
+ Prefer adding typed methods to `AppWorkflowService` instead of calling
104
+ `InngestService.create().client.send(...)` directly from routers or route
105
+ handlers. Keep a unit test that asserts the method sends the same payload shape
106
+ that `AppEvents` validates.
107
+
103
108
  ## Running Inngest Locally
104
109
 
105
- ### 1. Install the Inngest Dev Server
110
+ ### 1. Start the Inngest Dev Server
106
111
 
107
112
  ```bash
108
- pnpm dlx inngest-cli@latest dev
113
+ pnpm inngest:dev
109
114
  ```
110
115
 
111
- This starts the Inngest Dev Server at `http://localhost:8288` with a dashboard UI.
116
+ This starts the Inngest Dev Server for `http://localhost:3000/api/inngest` with a dashboard UI.
112
117
 
113
118
  ### 2. Set environment variables
114
119
 
@@ -126,7 +131,7 @@ INNGEST_EVENT_KEY=local # any value works locally
126
131
  pnpm dev
127
132
  ```
128
133
 
129
- The Inngest Dev Server auto-discovers functions by calling the serve endpoint at `/api/inngest`. Open `http://localhost:8288` to see registered functions, trigger events manually, and inspect runs.
134
+ The Inngest Dev Server auto-discovers functions by calling the serve endpoint at `/api/inngest`. Open the dashboard URL printed by `pnpm inngest:dev` to see registered functions, trigger events manually, and inspect runs.
130
135
 
131
136
  ## Environment Variables
132
137
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Langfuse is an open-source LLM observability platform. It captures traces, spans, and generations from LLM calls so you can debug prompt behavior, monitor latency/cost, and run evaluations. Think of it as "Datadog for LLM apps."
4
4
 
5
- **Our opinionated setup:** We integrate Langfuse via OpenTelemetry (OTEL), not the Langfuse SDK directly. OTEL auto-instruments the app (HTTP, DB, etc.), and Langfuse acts as a span processor that receives LLM-specific traces. This gives us unified tracing both traditional backend spans and LLM generations in the same trace.
5
+ **Our opinionated setup:** We integrate Langfuse via OpenTelemetry (OTEL), not the Langfuse SDK directly. OTEL auto-instruments the app (HTTP, DB, etc.), exports general traces to the environment's OTEL collector, and adds Langfuse as a span processor when `LANGFUSE_*` values are configured. This gives us unified tracing while still letting the app run when Langfuse is not available in an environment.
6
6
 
7
7
  ## When to Use Langfuse
8
8
 
@@ -18,24 +18,28 @@ Langfuse is an open-source LLM observability platform. It captures traces, spans
18
18
 
19
19
  ### OpenTelemetry + Langfuse Span Processor
20
20
 
21
- The template uses Next.js's instrumentation hook (called on server startup) to bootstrap OTEL with Langfuse:
21
+ The template uses Next.js's instrumentation hook (called on server startup) to bootstrap OTEL with both the environment collector and optional Langfuse:
22
22
 
23
23
  ```typescript
24
24
  import { LangfuseSpanProcessor } from "@langfuse/otel";
25
- import { NodeSDK } from "@opentelemetry/sdk-node";
25
+ import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
26
26
  import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
27
27
 
28
28
  const sdk = new NodeSDK({
29
- spanProcessors: [new LangfuseSpanProcessor({ baseUrl, publicKey, secretKey })],
29
+ spanProcessors: [
30
+ new tracing.BatchSpanProcessor(otlpTraceExporter),
31
+ new LangfuseSpanProcessor({ baseUrl, publicKey, secretKey }),
32
+ ],
30
33
  instrumentations: [getNodeAutoInstrumentations()],
31
34
  });
32
35
  sdk.start();
33
36
  ```
34
37
 
35
38
  - `getNodeAutoInstrumentations()` automatically instruments HTTP calls, database queries, and other standard Node.js operations.
36
- - `LangfuseSpanProcessor` forwards spans to Langfuse. LLM spans (from the Vercel AI SDK or OpenAI SDK) are recognized and displayed as generations in the Langfuse UI.
39
+ - The OTLP span processor forwards general server traces to the environment collector.
40
+ - `LangfuseSpanProcessor` forwards only AI SDK spans to Langfuse when all `LANGFUSE_*` variables are set. General HTTP, DB, and server spans stay in the OTEL/LGTM pipeline and are not sent to Langfuse by default.
37
41
 
38
- **You do not need to manually instrument LLM calls.** If you use the Vercel AI SDK (`ai` package) or the OpenAI SDK, OTEL picks them up automatically.
42
+ **You do not need to manually instrument LLM calls.** Use `LLMService` from `src/services/llm/LLMService.ts`; it calls the Vercel AI SDK with telemetry enabled and provider/model metadata attached.
39
43
 
40
44
  ### LangfuseService (Direct API)
41
45
 
@@ -58,12 +62,9 @@ This is a singleton — `create()` returns the same instance every time. It grac
58
62
 
59
63
  ### Percepta's Internal Instance
60
64
 
61
- Percepta runs a shared Langfuse instance. To get keys:
65
+ Percepta runs a shared Langfuse instance. For `percepta-test` deploys, the generated Ryvn installation uses the shared demo project by inheriting `LANGFUSE_PUBLIC_KEY` and sensitive `LANGFUSE_SECRET_KEY` from the `demos-commons` variable group.
62
66
 
63
- 1. Ask your team lead for access to the Percepta Langfuse instance
64
- 2. Log into the Langfuse dashboard
65
- 3. Go to **Settings → API Keys** and create a new key pair
66
- 4. You'll get a **public key** and **secret key**
67
+ For non-demo environments, verify the target Ryvn environment has a Langfuse installation or shared Langfuse project, then store the project keys in an environment-scoped variable group or installation secrets.
67
68
 
68
69
  ### Self-Hosted / Langfuse Cloud
69
70
 
@@ -75,10 +76,10 @@ For external projects, you can:
75
76
 
76
77
  ### Option 1: Use Percepta's Langfuse (recommended)
77
78
 
78
- Set the keys in `.env.local` pointing to the shared instance:
79
+ Set the keys in `.env.local` if you want local traces to go to the shared instance:
79
80
 
80
81
  ```bash
81
- LANGFUSE_BASE_URL=https://langfuse.internal.percepta.ai # ask team for actual URL
82
+ LANGFUSE_BASE_URL=https://langfuse.percepta-test.aitco.dev
82
83
  LANGFUSE_PUBLIC_KEY=pk-lf-...
83
84
  LANGFUSE_SECRET_KEY=sk-lf-...
84
85
  ```
@@ -104,7 +105,7 @@ LANGFUSE_SECRET_KEY=sk-lf-...
104
105
 
105
106
  ### Option 3: Skip Langfuse entirely
106
107
 
107
- Simply don't set the `LANGFUSE_*` env vars. The instrumentation gracefully skips the Langfuse span processor, and `LangfuseService.isConfigured()` returns `false`. OTEL still instruments the app for general tracing.
108
+ Simply don't set the `LANGFUSE_*` env vars. This is the default local development path unless you are specifically debugging LLM telemetry. The instrumentation gracefully skips the Langfuse span processor, and `LangfuseService.isConfigured()` returns `false`. OTEL still instruments the app for general tracing when an OTLP collector endpoint is configured.
108
109
 
109
110
  ## Environment Variables
110
111
 
@@ -0,0 +1,59 @@
1
+ # LLM Calls
2
+
3
+ Use this guide when the app needs backend model calls, streaming responses, or structured generation.
4
+
5
+ ## Default Pattern
6
+
7
+ Use `LLMService` from `src/services/llm/LLMService.ts`. Do not instantiate provider clients directly in routes, tRPC routers, or Inngest functions.
8
+
9
+ ```typescript
10
+ import { LLMService } from "@/services/llm/LLMService";
11
+
12
+ const result = await LLMService.create().generateText({
13
+ telemetryFunctionId: "summarize-document",
14
+ system: "You summarize documents for internal operators.",
15
+ prompt: `Summarize this document:\n\n${documentText}`,
16
+ });
17
+
18
+ return result.text;
19
+ ```
20
+
21
+ For streaming:
22
+
23
+ ```typescript
24
+ const stream = LLMService.create().streamText({
25
+ telemetryFunctionId: "chat-response",
26
+ system: "You are a concise assistant.",
27
+ messages,
28
+ });
29
+ ```
30
+
31
+ ## Providers
32
+
33
+ `LlmProviderService` chooses a provider at call time:
34
+
35
+ 1. `LLM_PROVIDER`, when explicitly set.
36
+ 2. Anthropic when `ANTHROPIC_API_KEY` is available.
37
+ 3. OpenAI when `OPENAI_API_KEY` is available.
38
+
39
+ `LLM_MODEL` can override the default model. A per-call `modelId` or `provider` passed to `LLMService` overrides env defaults.
40
+
41
+ ## Deployment
42
+
43
+ For `percepta-test`, `deploy:percepta-test` attaches the `demos-commons` Ryvn variable group. That group provides the shared `ANTHROPIC_API_KEY` and Langfuse project keys, so generated demo apps do not need per-app LLM secrets.
44
+
45
+ ## Local Development
46
+
47
+ Local LLM calls are optional. If needed, set a provider key once in your shell profile or `~/.config/percepta/create.env`:
48
+
49
+ ```bash
50
+ ANTHROPIC_API_KEY=sk-ant-...
51
+ ```
52
+
53
+ `pnpm dev` loads `~/.config/percepta/create.env` automatically. Do not put shared demo keys in committed files. Do not require local Langfuse or a local LGTM stack unless the task is specifically about telemetry.
54
+
55
+ ## Observability
56
+
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. In `percepta-test`, those values are inherited from the environment.
58
+
59
+ Use a stable `telemetryFunctionId` for every meaningful LLM operation, such as `extract-invoice-fields` or `draft-member-message`.
@@ -78,12 +78,16 @@ Cover:
78
78
  2. **API routes** — which tRPC routers and procedures
79
79
  3. **Pages** — which routes/pages in the app
80
80
  4. **Background jobs** — which Inngest events and functions (if any)
81
- 5. **LLM integration** — which service to use, how to wire it (if any)
81
+ 5. **LLM integration** — use `LLMService` from `src/services/llm/LLMService.ts` when model calls are needed
82
82
 
83
83
  ### Decision: Langfuse
84
84
 
85
85
  Read [agent-skills/langfuse.md](langfuse.md) to understand Langfuse. **Use Langfuse if the app calls LLMs.** Skip it otherwise. The template already has the integration wired — you just need to ensure env vars are set.
86
86
 
87
+ ### Decision: LLM Calls
88
+
89
+ Read [agent-skills/llm.md](llm.md) when the app calls a model. Use `LLMService` for backend model calls and set a stable `telemetryFunctionId` for each LLM operation. Do not create provider clients directly in app code.
90
+
87
91
  ### Decision: Inngest
88
92
 
89
93
  Read [agent-skills/inngest.md](inngest.md) to understand Inngest. **Use Inngest if the app has background jobs, scheduled tasks, or multi-step agent pipelines.** The template has the scaffold — you add events and functions.
@@ -125,6 +129,7 @@ If it fails, fix the errors before moving to the next chunk. Do not accumulate b
125
129
  - **Use robust loading states.** `@percepta/components` is optional; add it first if you want `AsyncContent`.
126
130
  - **Use `getEnvConfig()` for env vars.** Never use `process.env` directly.
127
131
  - **Use `getLogger()` for logging.** Never use `console.log`. Use safe/unsafe field separation.
132
+ - **Use `LLMService` for model calls.** It handles provider selection and Langfuse/OTEL telemetry metadata.
128
133
  - **Follow the singleton pattern** for new services. Follow the existing `DatabaseService` singleton pattern in the codebase.
129
134
  - **Use `protectedProcedure`** for authenticated tRPC routes.
130
135
 
@@ -141,6 +146,14 @@ pnpm docker:up
141
146
  pnpm db:setup-and-migrate
142
147
  ```
143
148
 
149
+ If the app uses Inngest functions, start the local Inngest dev server in a separate terminal:
150
+
151
+ ```bash
152
+ pnpm inngest:dev
153
+ ```
154
+
155
+ Do not start local LGTM, OTEL collector, Faro, or Langfuse services unless the task is specifically about telemetry. Those integrations are optional locally and are wired by the Ryvn environment for deploys.
156
+
144
157
  ### Step 2: Start the dev server
145
158
 
146
159
  ```bash
@@ -180,7 +193,7 @@ git add -A
180
193
  git commit -m "Initial implementation of <app-name>"
181
194
 
182
195
  # Create the repo under the Percepta-Core org
183
- gh repo create Percepta-Core/<app-name> --private --source=. --push
196
+ gh repo create Percepta-Core/<app-name> --internal --source=. --push
184
197
  ```
185
198
 
186
199
  If `gh` is not authenticated, tell the user to run `gh auth login` and then continue.
@@ -8,7 +8,7 @@ Ryvn is the deployment platform used by Percepta to manage cloud infrastructure.
8
8
  |-------------|---------|---------------|
9
9
  | `percepta-test` | Internal dev/test | `<app>.percepta-test.aitco.dev` |
10
10
 
11
- New apps are deployed to **percepta-test**. This environment has a shared PostgreSQL instance, Inngest server, Langfuse, and OTEL collector.
11
+ New apps are deployed to **percepta-test** using the existing-environment deploy motion. This environment must already have shared PostgreSQL, Inngest, OTEL collector, LGTM stack, and Langfuse installations before app deploys run. The service installation points at the shared Langfuse URL and attaches the `demos-commons` variable group for shared demo Langfuse project keys.
12
12
 
13
13
  ## Deploying This App
14
14