@percepta/create 3.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 +93 -0
- package/dist/chunk-GEVZERMP.js +108 -0
- package/dist/chunk-R4FWPE4A.js +49 -0
- package/dist/chunk-WMJT7CB5.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +974 -0
- package/dist/init-Z4VGBHAK.js +96 -0
- package/dist/status-MITGDLTT.js +76 -0
- package/dist/sync-J4SFZHDX.js +136 -0
- package/dist/upstream-AQI7P4EU.js +144 -0
- package/package.json +58 -0
- package/template-versions.json +4 -0
- package/templates/library/README.md +30 -0
- package/templates/library/eslint.config.js +10 -0
- package/templates/library/gitignore.template +18 -0
- package/templates/library/package.json.template +29 -0
- package/templates/library/src/index.ts +9 -0
- package/templates/library/tsconfig.json +19 -0
- package/templates/monorepo/README.md +41 -0
- package/templates/monorepo/eslint.config.js +10 -0
- package/templates/monorepo/gitignore.template +31 -0
- package/templates/monorepo/npmrc.template +4 -0
- package/templates/monorepo/package.json.template +25 -0
- package/templates/monorepo/packages/.gitkeep +0 -0
- package/templates/monorepo/pnpm-workspace.yaml +2 -0
- package/templates/monorepo/tsconfig.json +16 -0
- package/templates/webapp/.claude/commands/sync.md +19 -0
- package/templates/webapp/.claude/commands/upstream.md +17 -0
- package/templates/webapp/.dockerignore +59 -0
- package/templates/webapp/.gitattributes +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +114 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +28 -0
- package/templates/webapp/.github/workflows/ci.yml +149 -0
- package/templates/webapp/.node-version +2 -0
- package/templates/webapp/.prettierrc.mjs +5 -0
- package/templates/webapp/AGENTS.md +240 -0
- package/templates/webapp/Dockerfile +64 -0
- package/templates/webapp/README.md +200 -0
- package/templates/webapp/agent-skills/database.md +140 -0
- package/templates/webapp/agent-skills/deploy.md +94 -0
- package/templates/webapp/agent-skills/inngest.md +147 -0
- package/templates/webapp/agent-skills/langfuse.md +117 -0
- package/templates/webapp/agent-skills/oneshot.md +216 -0
- package/templates/webapp/agent-skills/ryvn.md +25 -0
- package/templates/webapp/deploy/README.md +39 -0
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +11 -0
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +121 -0
- package/templates/webapp/docker-compose.yml +19 -0
- package/templates/webapp/drizzle.config.ts +30 -0
- package/templates/webapp/env.example.template +44 -0
- package/templates/webapp/eslint.config.mjs +52 -0
- package/templates/webapp/gitignore.template +53 -0
- package/templates/webapp/next.config.ts +8 -0
- package/templates/webapp/npmrc.template +4 -0
- package/templates/webapp/package.json.template +122 -0
- package/templates/webapp/postcss.config.mjs +5 -0
- package/templates/webapp/scripts/create-user.ts +47 -0
- package/templates/webapp/scripts/migrate.ts +18 -0
- package/templates/webapp/scripts/seed.ts +62 -0
- package/templates/webapp/scripts/setup-database.ts +57 -0
- package/templates/webapp/scripts/setup-readonly-user.ts +193 -0
- package/templates/webapp/scripts/start.sh +52 -0
- package/templates/webapp/src/app/(app)/layout.tsx +21 -0
- package/templates/webapp/src/app/(app)/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +103 -0
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/layout.tsx +15 -0
- package/templates/webapp/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/webapp/src/app/api/healthz/route.ts +10 -0
- package/templates/webapp/src/app/api/inngest/route.ts +31 -0
- package/templates/webapp/src/app/api/readyz/route.ts +31 -0
- package/templates/webapp/src/app/api/trpc/[trpc]/route.ts +21 -0
- package/templates/webapp/src/app/favicon.ico +0 -0
- package/templates/webapp/src/app/global-error.tsx +27 -0
- package/templates/webapp/src/app/layout.tsx +18 -0
- package/templates/webapp/src/components/FaroProvider.tsx +37 -0
- package/templates/webapp/src/components/Header.tsx +70 -0
- package/templates/webapp/src/components/Providers.tsx +45 -0
- package/templates/webapp/src/components/form/FormItem.tsx +82 -0
- package/templates/webapp/src/config/clientEnvConfig.ts +11 -0
- package/templates/webapp/src/config/getEnvConfig.ts +62 -0
- package/templates/webapp/src/config/isDev.ts +7 -0
- package/templates/webapp/src/drizzle/db.ts +28 -0
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +57 -0
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +376 -0
- package/templates/webapp/src/drizzle/migrations/meta/_journal.json +13 -0
- package/templates/webapp/src/drizzle/schema/auth/accounts.ts +33 -0
- package/templates/webapp/src/drizzle/schema/auth/sessions.ts +25 -0
- package/templates/webapp/src/drizzle/schema/auth/users.ts +38 -0
- package/templates/webapp/src/drizzle/schema/auth/verifications.ts +19 -0
- package/templates/webapp/src/drizzle/schema/index.ts +4 -0
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +25 -0
- package/templates/webapp/src/instrumentation.ts +35 -0
- package/templates/webapp/src/lib/auth/index.ts +85 -0
- package/templates/webapp/src/lib/auth-client.ts +6 -0
- package/templates/webapp/src/lib/trpc.ts +15 -0
- package/templates/webapp/src/server/api/root.ts +5 -0
- package/templates/webapp/src/server/trpc.ts +61 -0
- package/templates/webapp/src/services/AuthContextService.ts +63 -0
- package/templates/webapp/src/services/DatabaseService.ts +54 -0
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +5 -0
- package/templates/webapp/src/services/inngest/InngestService.ts +71 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +34 -0
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +14 -0
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +80 -0
- package/templates/webapp/src/services/logger/AppLogger.ts +61 -0
- package/templates/webapp/src/services/logger/withRequestContext.ts +27 -0
- package/templates/webapp/src/services/observability/initFaro.ts +22 -0
- package/templates/webapp/src/startup-checks.ts +32 -0
- package/templates/webapp/src/styles/globals.css +27 -0
- package/templates/webapp/src/utils/__tests__/cn.test.ts +20 -0
- package/templates/webapp/src/utils/cn.ts +6 -0
- package/templates/webapp/src/utils/syncInngestApp.ts +62 -0
- package/templates/webapp/terraform/README.md +147 -0
- package/templates/webapp/terraform/deploy.sh +97 -0
- package/templates/webapp/terraform/main.tf +101 -0
- package/templates/webapp/terraform/modules/cloudtrail/main.tf +27 -0
- package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/cloudtrail/variables.tf +15 -0
- package/templates/webapp/terraform/modules/networking/main.tf +118 -0
- package/templates/webapp/terraform/modules/networking/outputs.tf +38 -0
- package/templates/webapp/terraform/modules/networking/variables.tf +24 -0
- package/templates/webapp/terraform/modules/rds/main.tf +227 -0
- package/templates/webapp/terraform/modules/rds/outputs.tf +73 -0
- package/templates/webapp/terraform/modules/rds/variables.tf +61 -0
- package/templates/webapp/terraform/modules/s3-logging/main.tf +148 -0
- package/templates/webapp/terraform/modules/s3-logging/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/s3-logging/variables.tf +16 -0
- package/templates/webapp/terraform/modules/secrets/main.tf +39 -0
- package/templates/webapp/terraform/modules/secrets/outputs.tf +9 -0
- package/templates/webapp/terraform/modules/secrets/variables.tf +51 -0
- package/templates/webapp/terraform/outputs.tf +102 -0
- package/templates/webapp/terraform/providers.tf +32 -0
- package/templates/webapp/terraform/terraform.tfvars.example +65 -0
- package/templates/webapp/terraform/variables.tf +129 -0
- package/templates/webapp/tsconfig.json +14 -0
- package/templates/webapp/vitest.config.ts +9 -0
- package/templates/webapp/vitest.setup.ts +5 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Webapp Template
|
|
2
|
+
|
|
3
|
+
Next.js 15 full-stack application scaffolded from the Mosaic webapp template via `@percepta/create`. Uses React 19, TypeScript, Tailwind CSS v4, tRPC, Drizzle ORM, Better Auth, and Inngest.
|
|
4
|
+
|
|
5
|
+
## Build & Dev Commands
|
|
6
|
+
|
|
7
|
+
- `pnpm dev` — start dev server with Turbopack
|
|
8
|
+
- `pnpm build` — production build
|
|
9
|
+
- `pnpm lint` — run ESLint
|
|
10
|
+
- `pnpm test` — run Vitest tests
|
|
11
|
+
- `pnpm docker:up` / `pnpm docker:down` — start/stop PostgreSQL
|
|
12
|
+
- `pnpm db:generate` — generate Drizzle migrations
|
|
13
|
+
- `pnpm db:migrate` — apply migrations
|
|
14
|
+
- `pnpm db:setup-and-migrate` — create DB + migrate
|
|
15
|
+
- `pnpm db:studio` — open Drizzle Studio
|
|
16
|
+
- `pnpm db:seed` — seed default dev user (admin@example.com / password)
|
|
17
|
+
|
|
18
|
+
**Package manager**: Always use `pnpm`, never `npm` or `yarn`.
|
|
19
|
+
|
|
20
|
+
## Code Style
|
|
21
|
+
|
|
22
|
+
- Double quotes for strings
|
|
23
|
+
- `no-console` is enforced — use `@percepta/logger` instead (see below)
|
|
24
|
+
- Logger messages must be plain string literals, not variables or templates
|
|
25
|
+
- `no-process-env` is enforced — use `getEnvConfig()` from `src/config/`
|
|
26
|
+
- Use `@percepta/design` components before writing custom UI
|
|
27
|
+
- Use `AsyncContent` from `@percepta/components` for loading/error states
|
|
28
|
+
- Tailwind CSS for all styling; icons from `lucide-react`
|
|
29
|
+
|
|
30
|
+
## Project Structure
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
src/ # Application source
|
|
34
|
+
├── app/ # Next.js App Router pages, layouts, and API route handlers
|
|
35
|
+
├── components/ # React components
|
|
36
|
+
├── config/ # Env config (clientEnvConfig, getEnvConfig)
|
|
37
|
+
├── drizzle/ # Schema definitions & SQL migrations
|
|
38
|
+
│ ├── schema/
|
|
39
|
+
│ └── migrations/
|
|
40
|
+
├── lib/ # Auth config, tRPC client, API middleware
|
|
41
|
+
├── server/ # tRPC router definitions
|
|
42
|
+
│ ├── trpc.ts # Context & procedure builders
|
|
43
|
+
│ └── api/root.ts # Root appRouter
|
|
44
|
+
├── services/
|
|
45
|
+
│ ├── inngest/ # Background job definitions
|
|
46
|
+
│ ├── langfuse/ # LLM observability
|
|
47
|
+
│ ├── logger/ # App logger setup (wraps @percepta/logger)
|
|
48
|
+
│ └── observability/ # OpenTelemetry setup
|
|
49
|
+
└── utils/ # Helpers (cn, pathEncryption, etc.)
|
|
50
|
+
|
|
51
|
+
deploy/ # Infrastructure-as-code for Ryvn deployments
|
|
52
|
+
└── ryvn/
|
|
53
|
+
├── __APP_NAME__.service.yaml # Ryvn Service definition
|
|
54
|
+
└── environments/<env>/installations/__APP_NAME__.env.<env>.serviceinstallation.yaml
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## @percepta Packages
|
|
58
|
+
|
|
59
|
+
### @percepta/design — UI Component Library
|
|
60
|
+
|
|
61
|
+
47+ accessible components built on Radix UI. Import styles and theme in your app:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// src/app/layout.tsx
|
|
65
|
+
import "@percepta/design/styles";
|
|
66
|
+
|
|
67
|
+
// src/styles/globals.css
|
|
68
|
+
@import "@percepta/design/theme";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Tailwind v4 + globals.css:** This project uses Tailwind CSS v4, which is CSS-based — there is no `tailwind.config.js`. Do not create one.
|
|
72
|
+
|
|
73
|
+
- Import order in globals.css matters: `@import "tailwindcss"` must come before `@import "@percepta/design/theme"` so design tokens layer correctly
|
|
74
|
+
- Custom theme values go in the `@theme inline` block in CSS, not a JS config file
|
|
75
|
+
- Use `@import "tailwindcss"`, not the old v3 directives (`@tailwind base`, `@tailwind components`, `@tailwind utilities`)
|
|
76
|
+
|
|
77
|
+
Import components directly:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import {
|
|
81
|
+
Button, Dialog, DropdownMenu, Select, Tabs,
|
|
82
|
+
Input, Checkbox, Switch, Card, Badge, Table,
|
|
83
|
+
Tooltip, Popover, Accordion, Avatar, Calendar,
|
|
84
|
+
// ... 47+ components available
|
|
85
|
+
} from "@percepta/design";
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Also includes composite components: `ButtonWithDropdown`, `Combobox`, `IconButton`, `InputGroup`, `MosaicDialog`, `MarkdownEditor`.
|
|
89
|
+
|
|
90
|
+
**Discovering components and props:** These are shadcn-style components wrapping Radix UI, shipped as an npm package. To explore what's available:
|
|
91
|
+
|
|
92
|
+
- Full export list: read `node_modules/@percepta/design/dist/src/index.d.ts`
|
|
93
|
+
- Component props and variants: read `node_modules/@percepta/design/dist/src/components/ui/<name>.d.ts` (e.g., `button.d.ts`)
|
|
94
|
+
- Composite components: `node_modules/@percepta/design/dist/src/components/composite/<Name>.d.ts`
|
|
95
|
+
- Props follow standard Radix UI patterns — if you know Radix, you know these components
|
|
96
|
+
|
|
97
|
+
### @percepta/build — Shared Build Configs
|
|
98
|
+
|
|
99
|
+
Provides centralized ESLint, Prettier, TypeScript, and Vitest configuration.
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
// eslint.config.mjs
|
|
103
|
+
import createEslintConfig from "@percepta/build/eslint";
|
|
104
|
+
export default [
|
|
105
|
+
...createEslintConfig({ type: "react", dirname: import.meta.dirname }),
|
|
106
|
+
// app-specific overrides...
|
|
107
|
+
];
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
// tsconfig.json
|
|
112
|
+
{ "extends": "@percepta/build/tsconfig/web" }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Vitest config is also available via `@percepta/build/vitest`.
|
|
116
|
+
|
|
117
|
+
### @percepta/components — React Utilities
|
|
118
|
+
|
|
119
|
+
Async data handling and hooks for React Query:
|
|
120
|
+
|
|
121
|
+
- **`AsyncContent<T>`** — renders loading spinner, error state, or children based on a React Query result. Use this for all data-fetching UI.
|
|
122
|
+
- **`AsyncArrayContent<T[]>`** — same but for multiple parallel queries
|
|
123
|
+
- **`ErrorContainer`** — consistent error display
|
|
124
|
+
- **`useBoolean`** — boolean state with `setTrue`, `setFalse`, `toggle`
|
|
125
|
+
- **`useDebouncedUpdate`** — debounced value updates with callback
|
|
126
|
+
- **`useSafeParseQueryParams`** — Zod-based URL query param validation
|
|
127
|
+
|
|
128
|
+
### @percepta/logger — Structured Logging
|
|
129
|
+
|
|
130
|
+
PII-safe logging with `safe`/`unsafe` field separation. Unsafe data is automatically redacted.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { getLogger } from "@/services/logger/AppLogger";
|
|
134
|
+
const logger = getLogger();
|
|
135
|
+
|
|
136
|
+
// safe data appears as-is; unsafe data (PII) is redacted
|
|
137
|
+
logger.info({ safe: { requestId }, unsafe: { email } }, "User action completed");
|
|
138
|
+
|
|
139
|
+
// errors go as the third argument
|
|
140
|
+
logger.error({ safe: { documentId } }, "Processing failed", error);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The app's logger is initialized in `src/services/logger/AppLogger.ts` using `createLogFactory()` and `createTracerFactory()` from this package. It uses `AsyncLocalStorage` for automatic request context propagation.
|
|
144
|
+
|
|
145
|
+
### @percepta/utils — Shared Utilities
|
|
146
|
+
|
|
147
|
+
- **`assertNever(value)`** — TypeScript exhaustiveness check (throws on impossible cases)
|
|
148
|
+
- **`batchAsync(items, fn, concurrency)`** — process items in batched parallel
|
|
149
|
+
- **`getEnvConfig()`** — typed environment variable loading
|
|
150
|
+
- **`zodKeyedTypeUnion()`** — Zod schemas for discriminated unions
|
|
151
|
+
|
|
152
|
+
### @percepta/next-utils — Next.js Middleware
|
|
153
|
+
|
|
154
|
+
Request context management and observability for Next.js:
|
|
155
|
+
|
|
156
|
+
- **`createRequestContextMiddleware()`** — middleware that adds `X-Request-ID` headers
|
|
157
|
+
- **`withRequestContext(handler)`** — wraps Pages Router API handlers
|
|
158
|
+
- **`withAppRouterRequestContext(handler)`** — wraps App Router handlers
|
|
159
|
+
- **`getRequestId()`** — extracts or generates request IDs
|
|
160
|
+
- **Faro integration** — `@percepta/next-utils/faro` for Grafana frontend observability
|
|
161
|
+
|
|
162
|
+
### @percepta/chat-components — Agent Chat UI
|
|
163
|
+
|
|
164
|
+
Pre-built components for conversational AI interfaces:
|
|
165
|
+
|
|
166
|
+
- **`ChatContainer`** — main chat wrapper (manages agent slug, threads, messages)
|
|
167
|
+
- **`MessageList`** — renders message history
|
|
168
|
+
- **`MessageSender`** — input UI for sending messages
|
|
169
|
+
- **`MosaicContextProvider`** — context provider for chat state
|
|
170
|
+
- Hooks: `useCreateThread`, `useMessages`, `useSendMessage`
|
|
171
|
+
|
|
172
|
+
Requires `@percepta/mosaic-typescript-sdk` as a peer dependency.
|
|
173
|
+
|
|
174
|
+
## Skill Guides
|
|
175
|
+
|
|
176
|
+
Detailed how-to guides for each major stack component. Read the relevant guide when working with that technology.
|
|
177
|
+
|
|
178
|
+
| Guide | File | When to read |
|
|
179
|
+
|-------|------|-------------|
|
|
180
|
+
| Background Jobs (Inngest) | [agent-skills/inngest.md](agent-skills/inngest.md) | Adding async tasks, scheduled jobs, or agent workflows |
|
|
181
|
+
| LLM Observability (Langfuse) | [agent-skills/langfuse.md](agent-skills/langfuse.md) | App uses LLMs and needs trace/eval monitoring |
|
|
182
|
+
| Database (Drizzle) | [agent-skills/database.md](agent-skills/database.md) | Adding tables, writing migrations, querying data |
|
|
183
|
+
| Deployment (Ryvn) | [agent-skills/ryvn.md](agent-skills/ryvn.md) | Ryvn overview and Percepta environment context |
|
|
184
|
+
| Deploy to percepta-test | [agent-skills/deploy.md](agent-skills/deploy.md) | Step-by-step deploy using the pre-scaffolded `deploy/ryvn/` IaC files |
|
|
185
|
+
| Build App (Oneshot) | [agent-skills/oneshot.md](agent-skills/oneshot.md) | Building a complete app from requirements end-to-end |
|
|
186
|
+
|
|
187
|
+
## Key Patterns
|
|
188
|
+
|
|
189
|
+
### tRPC
|
|
190
|
+
|
|
191
|
+
Type-safe API layer. Define routers in `src/server/api/`, compose in `root.ts`:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// src/server/api/root.ts
|
|
195
|
+
import { router } from "../trpc";
|
|
196
|
+
export const appRouter = router({ /* add routers here */ });
|
|
197
|
+
export type AppRouter = typeof appRouter;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Client-side usage via `src/lib/trpc.ts`.
|
|
201
|
+
|
|
202
|
+
### Authentication
|
|
203
|
+
|
|
204
|
+
Better Auth configured in `src/lib/auth/`. Email/password credentials enabled by default.
|
|
205
|
+
|
|
206
|
+
- **Server-side**: `auth.api.getSession({ headers: await headers() })` — get session in server components or tRPC context
|
|
207
|
+
- **Client-side**: `authClient.useSession()` — React hook from `src/lib/auth-client.ts`
|
|
208
|
+
- **Sign in**: `authClient.signIn.email({ email, password })` — client-side
|
|
209
|
+
- **Sign out**: `authClient.signOut()` — client-side
|
|
210
|
+
- **API route**: `src/app/api/auth/[...all]/route.ts` — Better Auth handler
|
|
211
|
+
- **Env vars**: `BETTER_AUTH_SECRET` (required), `BETTER_AUTH_URL` (defaults to `http://localhost:3000`)
|
|
212
|
+
|
|
213
|
+
### Background Jobs
|
|
214
|
+
|
|
215
|
+
Inngest for async task processing. Define functions in `src/services/inngest/`. Configure via `INNGEST_*` env vars.
|
|
216
|
+
|
|
217
|
+
### Database
|
|
218
|
+
|
|
219
|
+
PostgreSQL with Drizzle ORM. Schema in `src/drizzle/schema/`, migrations in `src/drizzle/migrations/`. Connection managed by `src/services/DatabaseService.ts`.
|
|
220
|
+
|
|
221
|
+
### Observability
|
|
222
|
+
|
|
223
|
+
OpenTelemetry initialized in `src/instrumentation.ts`. Langfuse for LLM tracking in `src/services/langfuse/`. Faro for frontend monitoring via `@percepta/next-utils/faro`.
|
|
224
|
+
|
|
225
|
+
## Deployment
|
|
226
|
+
|
|
227
|
+
To deploy this app to percepta-test, follow [agent-skills/deploy.md](agent-skills/deploy.md). The Ryvn service definition and percepta-test installation YAML are already scaffolded at `deploy/ryvn/` with all values filled in — deploy is mostly "copy these two files into the infra repo, open a PR, set three secrets in the Ryvn UI."
|
|
228
|
+
|
|
229
|
+
The release CI/CD workflow is already included at `.github/workflows/ryvn-release.yaml`.
|
|
230
|
+
|
|
231
|
+
For Ryvn CLI operations, use the `/use-ryvn` skill.
|
|
232
|
+
|
|
233
|
+
## Template Sync
|
|
234
|
+
|
|
235
|
+
This app tracks its template origin in `.mosaic-template.json`. Two Claude commands are available:
|
|
236
|
+
|
|
237
|
+
- **`/sync`** — pull downstream changes from the mosaic template into this app
|
|
238
|
+
- **`/upstream`** — propose app improvements back to the mosaic template
|
|
239
|
+
|
|
240
|
+
Both commands use `@percepta/create` CLI under the hood. Check `mosaic-template-notes.md` for documented intentional divergences from the template.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Base image with PNPM:
|
|
2
|
+
ARG NODE_VERSION=24.4.1
|
|
3
|
+
FROM node:${NODE_VERSION}-slim AS base
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
RUN npm install -g pnpm
|
|
6
|
+
|
|
7
|
+
# Build stage:
|
|
8
|
+
FROM base AS builder
|
|
9
|
+
|
|
10
|
+
COPY . .
|
|
11
|
+
|
|
12
|
+
# Create .npmrc with build-time token for private packages:
|
|
13
|
+
ARG NPM_TOKEN
|
|
14
|
+
RUN echo "@percepta:registry=https://registry.npmjs.org/" > .npmrc && \
|
|
15
|
+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
|
|
16
|
+
|
|
17
|
+
# Add BASE_PATH as a build argument
|
|
18
|
+
ARG BASE_PATH
|
|
19
|
+
ENV BASE_PATH=${BASE_PATH}
|
|
20
|
+
|
|
21
|
+
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|
22
|
+
|
|
23
|
+
RUN NODE_ENV=production NODE_OPTIONS="--max-old-space-size=4096" pnpm build
|
|
24
|
+
|
|
25
|
+
# Remove .npmrc for security (contains auth token):
|
|
26
|
+
RUN rm -f .npmrc
|
|
27
|
+
|
|
28
|
+
# Production stage - create the final image:
|
|
29
|
+
FROM base AS production
|
|
30
|
+
|
|
31
|
+
# Install PostgreSQL client:
|
|
32
|
+
USER root
|
|
33
|
+
RUN apt-get update \
|
|
34
|
+
&& apt-get install -y --no-install-recommends \
|
|
35
|
+
postgresql-client \
|
|
36
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
37
|
+
|
|
38
|
+
# Set production environment:
|
|
39
|
+
ENV NODE_ENV=production
|
|
40
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
41
|
+
|
|
42
|
+
# Copy BASE_PATH from the builder stage
|
|
43
|
+
ENV BASE_PATH=${BASE_PATH}
|
|
44
|
+
|
|
45
|
+
# Copy built app from builder stage
|
|
46
|
+
COPY --from=builder /app/.next/standalone ./
|
|
47
|
+
COPY --from=builder /app/.next/static ./.next/static
|
|
48
|
+
COPY --from=builder /app/public ./public
|
|
49
|
+
|
|
50
|
+
# Copy scripts and source files needed for start.sh and runtime
|
|
51
|
+
COPY --from=builder /app/scripts ./scripts
|
|
52
|
+
COPY --from=builder /app/src ./src
|
|
53
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Expose the port:
|
|
57
|
+
EXPOSE 3000
|
|
58
|
+
|
|
59
|
+
# Set correct permissions and user:
|
|
60
|
+
RUN chown -R 1000:1000 /app && chmod +x /app/scripts/start.sh
|
|
61
|
+
USER 1000
|
|
62
|
+
|
|
63
|
+
# Start the application:
|
|
64
|
+
CMD ["./scripts/start.sh"]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# __APP_TITLE__
|
|
2
|
+
|
|
3
|
+
A production-ready Next.js application with authentication, database, logging, background jobs, and infrastructure as code.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Next.js 15** with App Router
|
|
8
|
+
- **Authentication** via Better Auth with email/password credentials
|
|
9
|
+
- **Database** with PostgreSQL, Drizzle ORM, and migrations
|
|
10
|
+
- **Logging** with Pino and structured safe/unsafe data separation
|
|
11
|
+
- **Background Jobs** with Inngest
|
|
12
|
+
- **Observability** with OpenTelemetry and Langfuse integration
|
|
13
|
+
- **Infrastructure** with Terraform modules for AWS (RDS, S3, IAM)
|
|
14
|
+
- **Type Safety** with TypeScript and Zod schemas
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Start the Database
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm docker:up
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Initialize the Database
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm db:setup-and-migrate
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. Configure Environment
|
|
31
|
+
|
|
32
|
+
Copy `.env.example` to `.env.local` and configure your environment variables.
|
|
33
|
+
|
|
34
|
+
### 4. Start Development Server
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm dev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Open [http://localhost:3000](http://localhost:3000) to see your app.
|
|
41
|
+
|
|
42
|
+
## Project Structure
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/
|
|
46
|
+
├── app/ # Next.js App Router pages, layouts, and API route handlers
|
|
47
|
+
├── components/ # React components
|
|
48
|
+
├── config/ # Environment configuration
|
|
49
|
+
├── drizzle/ # Database schema and migrations
|
|
50
|
+
├── hooks/ # Custom React hooks
|
|
51
|
+
├── lib/ # Authentication and utilities
|
|
52
|
+
├── server/ # tRPC routers
|
|
53
|
+
├── services/ # Business logic services
|
|
54
|
+
│ ├── inngest/ # Background job definitions
|
|
55
|
+
│ ├── langfuse/ # LLM observability
|
|
56
|
+
│ └── logger/ # Structured logging
|
|
57
|
+
└── utils/ # Utility functions
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Available Scripts
|
|
61
|
+
|
|
62
|
+
| Script | Description |
|
|
63
|
+
|--------|-------------|
|
|
64
|
+
| `pnpm dev` | Start development server with Turbopack |
|
|
65
|
+
| `pnpm build` | Build for production |
|
|
66
|
+
| `pnpm start` | Start production server |
|
|
67
|
+
| `pnpm lint` | Run ESLint |
|
|
68
|
+
| `pnpm docker:up` | Start PostgreSQL container |
|
|
69
|
+
| `pnpm docker:down` | Stop PostgreSQL container |
|
|
70
|
+
| `pnpm db:generate` | Generate Drizzle migrations |
|
|
71
|
+
| `pnpm db:migrate` | Run database migrations |
|
|
72
|
+
| `pnpm db:setup` | Create database and user |
|
|
73
|
+
| `pnpm db:setup-and-migrate` | Setup and migrate database |
|
|
74
|
+
| `pnpm db:studio` | Open Drizzle Studio |
|
|
75
|
+
| `pnpm db:create-user` | Create a user account |
|
|
76
|
+
| `pnpm db:seed` | Seed default dev user |
|
|
77
|
+
|
|
78
|
+
## Logging
|
|
79
|
+
|
|
80
|
+
This template uses structured logging with safe/unsafe data separation to protect PII:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { getLogger } from "@/services/logger/AppLogger";
|
|
84
|
+
|
|
85
|
+
const logger = getLogger();
|
|
86
|
+
|
|
87
|
+
// Safe data appears as-is in logs
|
|
88
|
+
// Unsafe data (PII) is redacted
|
|
89
|
+
logger.info(
|
|
90
|
+
{ safe: { requestId }, unsafe: { email } },
|
|
91
|
+
"User action completed"
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Errors should be passed as the third parameter
|
|
95
|
+
logger.error(
|
|
96
|
+
{ safe: { documentId } },
|
|
97
|
+
"Processing failed",
|
|
98
|
+
error
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Authentication
|
|
103
|
+
|
|
104
|
+
This app uses [Better Auth](https://better-auth.com) for authentication with email/password credentials enabled by default.
|
|
105
|
+
|
|
106
|
+
Required environment variables:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
|
|
110
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
To create a dev user:
|
|
114
|
+
```bash
|
|
115
|
+
pnpm db:seed
|
|
116
|
+
# Creates admin@example.com / password
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Environment Variables
|
|
120
|
+
|
|
121
|
+
### Application
|
|
122
|
+
|
|
123
|
+
| Variable | Description | Default |
|
|
124
|
+
|----------|-------------|---------|
|
|
125
|
+
| `NODE_ENV` | Environment mode | `development` |
|
|
126
|
+
| `APP_BASE_URL` | Base URL for the app | - |
|
|
127
|
+
|
|
128
|
+
### Database
|
|
129
|
+
|
|
130
|
+
| Variable | Description | Default |
|
|
131
|
+
|----------|-------------|---------|
|
|
132
|
+
| `DATABASE_HOST` | PostgreSQL host | `localhost` |
|
|
133
|
+
| `DATABASE_PORT` | PostgreSQL port | `5434` |
|
|
134
|
+
| `DATABASE_USERNAME` | Database user | `postgres` |
|
|
135
|
+
| `DATABASE_PASSWORD` | Database password | `postgres` |
|
|
136
|
+
| `DATABASE_NAME` | Database name | `__DB_NAME__` |
|
|
137
|
+
| `DATABASE_USE_SSL` | Enable SSL | `false` |
|
|
138
|
+
|
|
139
|
+
### Security
|
|
140
|
+
|
|
141
|
+
| Variable | Description |
|
|
142
|
+
|----------|-------------|
|
|
143
|
+
| `ENCRYPTION_SECRET_KEY` | 32-character key for URL encryption |
|
|
144
|
+
|
|
145
|
+
Generate a secret key:
|
|
146
|
+
```bash
|
|
147
|
+
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Inngest (Background Jobs)
|
|
151
|
+
|
|
152
|
+
| Variable | Description |
|
|
153
|
+
|----------|-------------|
|
|
154
|
+
| `INNGEST_BASE_URL` | Inngest server URL |
|
|
155
|
+
| `INNGEST_SIGNING_KEY` | Inngest signing key |
|
|
156
|
+
| `INNGEST_EVENT_KEY` | Inngest event key |
|
|
157
|
+
|
|
158
|
+
### Langfuse (LLM Observability)
|
|
159
|
+
|
|
160
|
+
| Variable | Description |
|
|
161
|
+
|----------|-------------|
|
|
162
|
+
| `LANGFUSE_BASE_URL` | Langfuse server URL |
|
|
163
|
+
| `LANGFUSE_PUBLIC_KEY` | Langfuse public key |
|
|
164
|
+
| `LANGFUSE_SECRET_KEY` | Langfuse secret key |
|
|
165
|
+
|
|
166
|
+
## Local AWS Development
|
|
167
|
+
|
|
168
|
+
This application uses the default AWS SDK credential provider chain:
|
|
169
|
+
|
|
170
|
+
1. **Environment Variables**:
|
|
171
|
+
- `AWS_ACCESS_KEY_ID`
|
|
172
|
+
- `AWS_SECRET_ACCESS_KEY`
|
|
173
|
+
- `AWS_SESSION_TOKEN` (optional)
|
|
174
|
+
- `AWS_REGION` (defaults to `us-east-1`)
|
|
175
|
+
|
|
176
|
+
2. **AWS Credentials File**: Run `aws configure` or `aws sso login`
|
|
177
|
+
|
|
178
|
+
## Infrastructure (Terraform)
|
|
179
|
+
|
|
180
|
+
The `terraform/` directory contains modules for AWS infrastructure:
|
|
181
|
+
|
|
182
|
+
- **RDS**: PostgreSQL database
|
|
183
|
+
- **S3**: File storage with logging
|
|
184
|
+
- **Networking**: VPC endpoints
|
|
185
|
+
- **Secrets**: AWS Secrets Manager
|
|
186
|
+
- **IAM**: Roles and policies
|
|
187
|
+
|
|
188
|
+
See `terraform/README.md` for deployment instructions.
|
|
189
|
+
|
|
190
|
+
## Learn More
|
|
191
|
+
|
|
192
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
|
193
|
+
- [Drizzle ORM](https://orm.drizzle.team)
|
|
194
|
+
- [Better Auth](https://better-auth.com/docs)
|
|
195
|
+
- [Inngest](https://www.inngest.com/docs)
|
|
196
|
+
- [Langfuse](https://langfuse.com/docs)
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Database — PostgreSQL + Drizzle ORM
|
|
2
|
+
|
|
3
|
+
This template uses PostgreSQL as the database and Drizzle ORM as the query builder and migration tool. Drizzle is a TypeScript-first ORM — schemas are defined in TypeScript, queries are type-safe, and migrations are generated as plain SQL files.
|
|
4
|
+
|
|
5
|
+
## Adding a New Table
|
|
6
|
+
|
|
7
|
+
### 1. Define the schema
|
|
8
|
+
|
|
9
|
+
Create a new schema file alongside the existing ones:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
13
|
+
import { users } from "./auth/users";
|
|
14
|
+
|
|
15
|
+
export const documents = pgTable("documents", {
|
|
16
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
17
|
+
title: text("title").notNull(),
|
|
18
|
+
content: text("content"),
|
|
19
|
+
userId: uuid("user_id")
|
|
20
|
+
.notNull()
|
|
21
|
+
.references(() => users.id),
|
|
22
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
23
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Export from the schema index
|
|
28
|
+
|
|
29
|
+
Add a re-export for your new table in the schema index file:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export * from "./documents";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Generate the migration
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm db:generate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This creates a new SQL migration file. **Review the generated SQL** — Drizzle generates it automatically but you should verify it's correct.
|
|
42
|
+
|
|
43
|
+
### 4. Apply the migration
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm db:migrate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Querying Data
|
|
50
|
+
|
|
51
|
+
### Basic queries with DatabaseService
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { DatabaseService } from "@/services/DatabaseService";
|
|
55
|
+
import { documents } from "@/drizzle/schema";
|
|
56
|
+
import { eq } from "drizzle-orm";
|
|
57
|
+
|
|
58
|
+
const db = DatabaseService.create().getDatabase();
|
|
59
|
+
|
|
60
|
+
// Select
|
|
61
|
+
const docs = await db.select().from(documents).where(eq(documents.userId, userId));
|
|
62
|
+
|
|
63
|
+
// Insert
|
|
64
|
+
await db.insert(documents).values({ title: "New Doc", userId });
|
|
65
|
+
|
|
66
|
+
// Update
|
|
67
|
+
await db.update(documents).set({ title: "Updated" }).where(eq(documents.id, docId));
|
|
68
|
+
|
|
69
|
+
// Delete
|
|
70
|
+
await db.delete(documents).where(eq(documents.id, docId));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Transactions
|
|
74
|
+
|
|
75
|
+
`DatabaseService` propagates transactions via `AsyncLocalStorage` — any code running inside `createTransaction` automatically uses the same transaction, even across nested function calls:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const dbService = DatabaseService.create();
|
|
79
|
+
|
|
80
|
+
await dbService.createTransaction(async (txn) => {
|
|
81
|
+
// These both run in the same transaction
|
|
82
|
+
await txn.insert(documents).values({ title: "Doc 1", userId });
|
|
83
|
+
await txn.insert(documents).values({ title: "Doc 2", userId });
|
|
84
|
+
|
|
85
|
+
// If any nested function calls dbService.getDatabase(),
|
|
86
|
+
// it gets the same transaction — not a new connection
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If `createTransaction` is called while already inside a transaction, it reuses the existing one (no nested transactions).
|
|
91
|
+
|
|
92
|
+
## Running PostgreSQL Locally
|
|
93
|
+
|
|
94
|
+
### Start with Docker Compose
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pnpm docker:up
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This starts PostgreSQL 16 on port **5434** (not the default 5432, to avoid conflicts).
|
|
101
|
+
|
|
102
|
+
### Create the database and run migrations
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm db:setup-and-migrate
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This creates the database if it doesn't exist and applies all pending migrations.
|
|
109
|
+
|
|
110
|
+
### Inspect the database
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pnpm db:studio
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Opens Drizzle Studio in the browser — a visual database explorer.
|
|
117
|
+
|
|
118
|
+
### Stop PostgreSQL
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pnpm docker:down
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Environment Variables
|
|
125
|
+
|
|
126
|
+
| Variable | Default | Description |
|
|
127
|
+
|----------|---------|-------------|
|
|
128
|
+
| `DATABASE_HOST` | `localhost` | PostgreSQL host |
|
|
129
|
+
| `DATABASE_PORT` | `5434` | PostgreSQL port |
|
|
130
|
+
| `DATABASE_USERNAME` | `postgres` | Database user |
|
|
131
|
+
| `DATABASE_PASSWORD` | `postgres` | Database password |
|
|
132
|
+
| `DATABASE_NAME` | `__DB_NAME__` | Database name |
|
|
133
|
+
| `DATABASE_USE_SSL` | `false` | Enable SSL connections |
|
|
134
|
+
|
|
135
|
+
## Key Concepts
|
|
136
|
+
|
|
137
|
+
- **Schemas are TypeScript, migrations are SQL.** You define tables in TS, then `pnpm db:generate` creates the SQL diff. Never hand-write migration SQL.
|
|
138
|
+
- **DatabaseService is a singleton.** Call `DatabaseService.create()` anywhere — it always returns the same instance with the same connection pool.
|
|
139
|
+
- **Transaction propagation is automatic.** Code inside `createTransaction` gets a transaction; code outside gets the raw connection. `getDatabase()` returns whichever is active.
|
|
140
|
+
- **Port 5434.** The local Docker Compose uses port 5434 to avoid conflicting with any system PostgreSQL on 5432.
|