create-raredays-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +89 -0
  2. package/bin/index.js +1024 -0
  3. package/package.json +43 -0
  4. package/scaffold-fragments/billing/files/app/api/billing/webhook/route.ts +5 -0
  5. package/scaffold-fragments/billing/module.json +31 -0
  6. package/scaffold-fragments/billing/runtime/config.ts +8 -0
  7. package/scaffold-fragments/billing/runtime/exports.ts +6 -0
  8. package/scaffold-fragments/billing/runtime/imports.ts +6 -0
  9. package/scaffold-fragments/billing/runtime/ports.ts +3 -0
  10. package/scaffold-fragments/cms/module.json +29 -0
  11. package/scaffold-fragments/cms/runtime/config.ts +4 -0
  12. package/scaffold-fragments/cms/runtime/exports.ts +1 -0
  13. package/scaffold-fragments/cms/runtime/imports.ts +1 -0
  14. package/scaffold-fragments/email/module.json +30 -0
  15. package/scaffold-fragments/email/runtime/config.ts +1 -0
  16. package/scaffold-fragments/email/runtime/imports.ts +1 -0
  17. package/scaffold-fragments/email/runtime/ports.ts +1 -0
  18. package/scaffold-fragments/feature-flags/files/app/lib/flags.ts +48 -0
  19. package/scaffold-fragments/feature-flags/module.json +66 -0
  20. package/scaffold-fragments/i18n/files/messages/en-US.json +15 -0
  21. package/scaffold-fragments/i18n/module.json +9 -0
  22. package/scaffold-fragments/i18n/runtime/config.ts +7 -0
  23. package/scaffold-fragments/i18n/runtime/exports.ts +1 -0
  24. package/scaffold-fragments/i18n/runtime/imports.ts +2 -0
  25. package/scaffold-fragments/i18n/runtime/ports.ts +1 -0
  26. package/scaffold-fragments/identity/module.json +30 -0
  27. package/scaffold-fragments/identity/vendors/better-auth/files/app/api/auth/[...all]/route.ts +15 -0
  28. package/scaffold-fragments/identity/vendors/better-auth/files/app/lib/auth.ts +20 -0
  29. package/scaffold-fragments/jobs/module.json +12 -0
  30. package/scaffold-fragments/jobs/runtime/config.ts +4 -0
  31. package/scaffold-fragments/jobs/runtime/imports.ts +1 -0
  32. package/scaffold-fragments/jobs/runtime/ports.ts +1 -0
  33. package/scaffold-fragments/notifications/module.json +12 -0
  34. package/scaffold-fragments/notifications/runtime/config.ts +8 -0
  35. package/scaffold-fragments/notifications/runtime/imports.ts +5 -0
  36. package/scaffold-fragments/notifications/runtime/ports.ts +1 -0
  37. package/scaffold-fragments/organizations/module.json +12 -0
  38. package/scaffold-fragments/telemetry/module.json +43 -0
  39. package/scaffold-fragments/telemetry/runtime/config.ts +7 -0
  40. package/scaffold-fragments/telemetry/runtime/imports.ts +7 -0
  41. package/scaffold-fragments/telemetry/runtime/ports.ts +1 -0
  42. package/skillex/private/contribute.md +193 -0
  43. package/skillex/public/consumer.md +157 -0
  44. package/templates/client/.storybook/main.ts +77 -0
  45. package/templates/client/.storybook/preview.ts +12 -0
  46. package/templates/client/CLAUDE.md +50 -0
  47. package/templates/client/_claude/settings.json +107 -0
  48. package/templates/client/_github/PULL_REQUEST_TEMPLATE.md +19 -0
  49. package/templates/client/_github/dependabot.yml +48 -0
  50. package/templates/client/_github/workflows/ci.yml +104 -0
  51. package/templates/client/_gitignore +15 -0
  52. package/templates/client/_npmrc +1 -0
  53. package/templates/client/app/(app)/layout.tsx +49 -0
  54. package/templates/client/app/(marketing)/layout.tsx +21 -0
  55. package/templates/client/app/(marketing)/page.tsx +34 -0
  56. package/templates/client/app/api/backend/[...path]/route.ts +37 -0
  57. package/templates/client/app/api/dev/login/route.ts +14 -0
  58. package/templates/client/app/api/dev/logout/route.ts +18 -0
  59. package/templates/client/app/globals.css +22 -0
  60. package/templates/client/app/home-auth-controls.tsx +28 -0
  61. package/templates/client/app/layout.tsx +38 -0
  62. package/templates/client/app/lib/backend-client.ts +55 -0
  63. package/templates/client/app/lib/runtime.test.ts +38 -0
  64. package/templates/client/app/lib/runtime.ts +54 -0
  65. package/templates/client/app/protected/page.tsx +31 -0
  66. package/templates/client/app/theme.override.css +9 -0
  67. package/templates/client/instrumentation.ts +10 -0
  68. package/templates/client/mocks/browser.tsx +28 -0
  69. package/templates/client/mocks/handlers.ts +20 -0
  70. package/templates/client/mocks/node.ts +7 -0
  71. package/templates/client/next.config.ts +8 -0
  72. package/templates/client/oxfmt.config.ts +8 -0
  73. package/templates/client/oxlint.config.ts +8 -0
  74. package/templates/client/package.json +61 -0
  75. package/templates/client/postcss.config.mjs +1 -0
  76. package/templates/client/public/mockServiceWorker.js +349 -0
  77. package/templates/client/scripts/build-theme.mjs +18 -0
  78. package/templates/client/site.spec.ts +58 -0
  79. package/templates/client/skillex.json +12 -0
  80. package/templates/client/skills/repo.md +56 -0
  81. package/templates/client/stylelint.config.mjs +4 -0
  82. package/templates/client/tsconfig.json +11 -0
  83. package/templates/client/vitest.config.ts +27 -0
  84. package/templates/client/vitest.setup.ts +18 -0
  85. package/templates/full-stack/.storybook/main.ts +77 -0
  86. package/templates/full-stack/.storybook/preview.ts +12 -0
  87. package/templates/full-stack/CLAUDE.md +50 -0
  88. package/templates/full-stack/_claude/settings.json +107 -0
  89. package/templates/full-stack/_github/PULL_REQUEST_TEMPLATE.md +19 -0
  90. package/templates/full-stack/_github/dependabot.yml +57 -0
  91. package/templates/full-stack/_github/workflows/ci.yml +109 -0
  92. package/templates/full-stack/_github/workflows/db-check.yml +36 -0
  93. package/templates/full-stack/_github/workflows/db-migrate.yml +39 -0
  94. package/templates/full-stack/_gitignore +15 -0
  95. package/templates/full-stack/_npmrc +1 -0
  96. package/templates/full-stack/app/(app)/layout.tsx +53 -0
  97. package/templates/full-stack/app/(marketing)/layout.tsx +21 -0
  98. package/templates/full-stack/app/(marketing)/page.tsx +38 -0
  99. package/templates/full-stack/app/api/dev/login/route.ts +14 -0
  100. package/templates/full-stack/app/api/dev/logout/route.ts +18 -0
  101. package/templates/full-stack/app/api/widgets/route.ts +10 -0
  102. package/templates/full-stack/app/demo/widgets/page.tsx +86 -0
  103. package/templates/full-stack/app/globals.css +22 -0
  104. package/templates/full-stack/app/home-auth-controls.tsx +28 -0
  105. package/templates/full-stack/app/layout.tsx +38 -0
  106. package/templates/full-stack/app/lib/runtime.test.ts +38 -0
  107. package/templates/full-stack/app/lib/runtime.ts +54 -0
  108. package/templates/full-stack/app/protected/page.tsx +31 -0
  109. package/templates/full-stack/app/theme.override.css +9 -0
  110. package/templates/full-stack/db/check.ts +46 -0
  111. package/templates/full-stack/db/index.ts +109 -0
  112. package/templates/full-stack/db/migrate.ts +66 -0
  113. package/templates/full-stack/db/migrations/.gitkeep +0 -0
  114. package/templates/full-stack/db/schema.ts +20 -0
  115. package/templates/full-stack/drizzle.config.ts +16 -0
  116. package/templates/full-stack/instrumentation.ts +10 -0
  117. package/templates/full-stack/mocks/browser.tsx +28 -0
  118. package/templates/full-stack/mocks/handlers.ts +34 -0
  119. package/templates/full-stack/mocks/node.ts +7 -0
  120. package/templates/full-stack/next.config.ts +8 -0
  121. package/templates/full-stack/oxfmt.config.ts +8 -0
  122. package/templates/full-stack/oxlint.config.ts +8 -0
  123. package/templates/full-stack/package.json +72 -0
  124. package/templates/full-stack/postcss.config.mjs +1 -0
  125. package/templates/full-stack/public/mockServiceWorker.js +349 -0
  126. package/templates/full-stack/scripts/build-theme.mjs +18 -0
  127. package/templates/full-stack/site.spec.ts +58 -0
  128. package/templates/full-stack/skillex.json +12 -0
  129. package/templates/full-stack/skills/repo.md +56 -0
  130. package/templates/full-stack/stylelint.config.mjs +4 -0
  131. package/templates/full-stack/tsconfig.json +11 -0
  132. package/templates/full-stack/vitest.config.ts +42 -0
  133. package/templates/full-stack/vitest.setup.ts +18 -0
  134. package/templates/static/.storybook/main.ts +77 -0
  135. package/templates/static/.storybook/preview.ts +12 -0
  136. package/templates/static/CLAUDE.md +50 -0
  137. package/templates/static/_claude/settings.json +107 -0
  138. package/templates/static/_github/PULL_REQUEST_TEMPLATE.md +18 -0
  139. package/templates/static/_github/dependabot.yml +43 -0
  140. package/templates/static/_github/workflows/ci.yml +91 -0
  141. package/templates/static/_gitignore +15 -0
  142. package/templates/static/_npmrc +1 -0
  143. package/templates/static/app/(marketing)/layout.tsx +21 -0
  144. package/templates/static/app/(marketing)/page.tsx +17 -0
  145. package/templates/static/app/globals.css +22 -0
  146. package/templates/static/app/layout.tsx +24 -0
  147. package/templates/static/app/theme.override.css +9 -0
  148. package/templates/static/next.config.ts +19 -0
  149. package/templates/static/oxfmt.config.ts +8 -0
  150. package/templates/static/oxlint.config.ts +8 -0
  151. package/templates/static/package.json +53 -0
  152. package/templates/static/postcss.config.mjs +1 -0
  153. package/templates/static/scripts/build-theme.mjs +18 -0
  154. package/templates/static/site.spec.ts +58 -0
  155. package/templates/static/skillex.json +12 -0
  156. package/templates/static/skills/repo.md +56 -0
  157. package/templates/static/stylelint.config.mjs +4 -0
  158. package/templates/static/tsconfig.json +11 -0
@@ -0,0 +1,10 @@
1
+ // Demo endpoint paired with `/demo/widgets`. Returns an empty list by
2
+ // default so a fresh scaffold renders the Empty slot — flip MOCKS on
3
+ // to see canned data via MSW. Replace this handler with a real query
4
+ // once you have a backing store.
5
+
6
+ import { NextResponse } from "next/server";
7
+
8
+ export function GET(): NextResponse {
9
+ return NextResponse.json({ widgets: [], total: 0 });
10
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ // Demo of `defineAsyncLoad` + MSW.
4
+ //
5
+ // Without mocks (`pnpm dev`): /api/widgets returns an empty list, so the
6
+ // Empty slot renders with a nudge toward `pnpm dev:mocks`. With mocks
7
+ // on, MSW intercepts before the route fires and the Success slot
8
+ // renders canned data.
9
+ //
10
+ // Delete this `/demo` subtree once you have your own pages.
11
+
12
+ import { asyncSource, defineAsyncLoad } from "@raredays/core";
13
+
14
+ interface Widget {
15
+ id: string;
16
+ name: string;
17
+ inventory: number;
18
+ }
19
+
20
+ interface WidgetsData {
21
+ widgets: Widget[];
22
+ total: number;
23
+ }
24
+
25
+ const Widgets = defineAsyncLoad<WidgetsData>({
26
+ source: asyncSource(async () => {
27
+ const r = await fetch("/api/widgets");
28
+ if (!r.ok) throw new Error(`/api/widgets responded ${r.status}`);
29
+ return (await r.json()) as WidgetsData;
30
+ }),
31
+ Loading: () => <p data-testid="widgets-state">Loading widgets…</p>,
32
+ Empty: ({ retry }) => (
33
+ <div data-testid="widgets-state">
34
+ <p>No widgets yet.</p>
35
+ <p className="mt-1 text-sm text-neutral-600">
36
+ Run <code>pnpm dev:mocks</code> to see canned data, or add real ones via the API route at{" "}
37
+ <code>app/api/widgets/route.ts</code>.
38
+ </p>
39
+ <button
40
+ type="button"
41
+ onClick={retry}
42
+ className="mt-3 rounded border border-neutral-300 px-3 py-1 text-sm"
43
+ >
44
+ Reload
45
+ </button>
46
+ </div>
47
+ ),
48
+ Success: ({ widgets, total }) => (
49
+ <ul data-testid="widgets-state" className="mt-2 space-y-1">
50
+ <li className="text-sm text-neutral-600" data-testid="widgets-total">
51
+ {total} widget{total === 1 ? "" : "s"}
52
+ </li>
53
+ {widgets.map((w) => (
54
+ <li key={w.id} data-testid="widget-row">
55
+ <span className="font-medium">{w.name}</span>
56
+ <span className="ml-2 text-sm text-neutral-600">({w.inventory} in stock)</span>
57
+ </li>
58
+ ))}
59
+ </ul>
60
+ ),
61
+ Failure: ({ error, retry }) => (
62
+ <div data-testid="widgets-state">
63
+ <p className="text-red-700">Failed to load widgets: {error.message}</p>
64
+ <button
65
+ type="button"
66
+ onClick={retry}
67
+ className="mt-3 rounded border border-neutral-300 px-3 py-1 text-sm"
68
+ >
69
+ Retry
70
+ </button>
71
+ </div>
72
+ ),
73
+ });
74
+
75
+ export default function WidgetsDemoPage(): React.ReactElement {
76
+ return (
77
+ <main className="mx-auto max-w-2xl p-8 font-sans">
78
+ <h1 className="text-2xl font-bold">Widgets demo</h1>
79
+ <p className="mt-2 text-sm text-neutral-600">
80
+ <code>defineAsyncLoad</code> against <code>/api/widgets</code>. MSW intercepts in dev when{" "}
81
+ <code>NEXT_PUBLIC_MOCKS=on</code>.
82
+ </p>
83
+ <Widgets />
84
+ </main>
85
+ );
86
+ }
@@ -0,0 +1,22 @@
1
+ /* CSS @import lines must precede all other rules. If you add a remote-font
2
+ * import (e.g. @import url("https://rsms.me/inter/inter.css")), it MUST go
3
+ * BEFORE the lines below — @raredays/style inlines ~6800 lines of CSS,
4
+ * after which any further @import is mid-document and rejected by Lightning
5
+ * CSS / PostCSS / browsers per the CSS spec. */
6
+ @import "@raredays/style";
7
+ @import "./theme.override.css";
8
+
9
+ /* Scan compiled output from @raredays/* packages so Tailwind emits
10
+ * the arbitrary utilities used inside their components (e.g. underline
11
+ * positions, data-attribute variants). The relative `@source` paths in
12
+ * @raredays/style/index.css don't resolve under pnpm's strict
13
+ * node_modules layout — each @raredays/* package gets its own
14
+ * .pnpm/<pkg>/node_modules/<pkg>/ directory, so style's
15
+ * `../components/dist` points nowhere. These directives are relative
16
+ * to THIS file (`app/globals.css`), which is the consumer's cwd —
17
+ * resolves correctly.
18
+ *
19
+ * Add `@source "../node_modules/@raredays/<pkg>/dist"` for any
20
+ * additional @raredays/* package whose component utilities you use. */
21
+ @source "../node_modules/@raredays/components/dist";
22
+ @source "../node_modules/@raredays/core/dist";
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ export function HomeAuthControls(): React.ReactElement {
4
+ return (
5
+ <div className="mt-6 flex flex-wrap gap-2">
6
+ <button
7
+ type="button"
8
+ onClick={async () => {
9
+ await fetch("/api/dev/login", { method: "POST" });
10
+ window.location.href = "/protected";
11
+ }}
12
+ className="rounded-md bg-brand-primary px-4 py-2 text-white hover:bg-brand-secondary"
13
+ >
14
+ Sign in as Alice → /protected
15
+ </button>
16
+ <button
17
+ type="button"
18
+ onClick={async () => {
19
+ await fetch("/api/dev/logout", { method: "POST" });
20
+ window.location.reload();
21
+ }}
22
+ className="rounded-md border border-neutral-300 px-4 py-2 hover:bg-neutral-50"
23
+ >
24
+ Sign out
25
+ </button>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,38 @@
1
+ import { ThemeProvider, Toaster } from "@raredays/components";
2
+ import { RaredaysProvider } from "@raredays/core";
3
+ import type { Metadata, Viewport } from "next";
4
+ import type { ReactNode } from "react";
5
+ import { StartMocks } from "../mocks/browser";
6
+ import "./globals.css";
7
+
8
+ export const metadata: Metadata = {
9
+ title: "RareDays App",
10
+ description: "A RareDays Next.js application.",
11
+ };
12
+
13
+ export const viewport: Viewport = {
14
+ width: "device-width",
15
+ initialScale: 1,
16
+ };
17
+
18
+ export default function RootLayout({ children }: { children: ReactNode }) {
19
+ return (
20
+ <html lang="en" suppressHydrationWarning>
21
+ <body>
22
+ <StartMocks>
23
+ <RaredaysProvider>
24
+ <ThemeProvider
25
+ attribute="class"
26
+ defaultTheme="system"
27
+ enableSystem
28
+ disableTransitionOnChange
29
+ >
30
+ {children}
31
+ <Toaster position="bottom-center" />
32
+ </ThemeProvider>
33
+ </RaredaysProvider>
34
+ </StartMocks>
35
+ </body>
36
+ </html>
37
+ );
38
+ }
@@ -0,0 +1,38 @@
1
+ // @vitest-environment node
2
+ // Runs in the Node environment (not happy-dom) because the WHATWG `Request`
3
+ // in happy-dom strips the `cookie` header on construction (browsers forbid
4
+ // setting it from script). Server-side identity tests need the real Node
5
+ // `Request`, which preserves arbitrary headers.
6
+
7
+ import { describe, expect, it } from "vitest";
8
+ import { app, SEEDED } from "./runtime.js";
9
+
10
+ // Sample test demonstrating how to test app code against the framework's
11
+ // port contracts. The composition root provides the wired ports; tests
12
+ // drive them directly. For deeper testing of your own custom adapters,
13
+ // import the framework's conformance harnesses (e.g.
14
+ // `identityProviderConformance` from `@raredays/identity/testing`) and
15
+ // pass your factory.
16
+
17
+ describe("identity flow (in-memory provider)", () => {
18
+ it("resolves the seeded session when the dev cookie is present", async () => {
19
+ const provider = app.require("identityProvider");
20
+ const req = new Request("https://example.com/", {
21
+ headers: { cookie: `rd-dev-session=${SEEDED.sessionId}` },
22
+ });
23
+ const session = await provider.getSession(req);
24
+ expect(session).not.toBeNull();
25
+ expect(session?.user.id).toBe(SEEDED.user.id);
26
+ });
27
+
28
+ it("returns null when no session cookie is present", async () => {
29
+ const provider = app.require("identityProvider");
30
+ const session = await provider.getSession(new Request("https://example.com/"));
31
+ expect(session).toBeNull();
32
+ });
33
+
34
+ it("seeded user has content.read but not content.manage", () => {
35
+ expect(SEEDED.user.hasPermission("content.read")).toBe(true);
36
+ expect(SEEDED.user.hasPermission("content.manage")).toBe(false);
37
+ });
38
+ });
@@ -0,0 +1,54 @@
1
+ // Composition root. Every port the framework knows wires here; routes and
2
+ // helpers read ports from the resulting `RaredaysApp` instance.
3
+ //
4
+ // Default uses the in-memory IdentityProvider so `pnpm dev` runs out of the
5
+ // box. To go to production:
6
+ // 1. Install `@raredays/identity-better-auth`, configure Better Auth
7
+ // against your Drizzle database.
8
+ // 2. Replace `createInMemoryProvider(...)` below with
9
+ // `createBetterAuthProvider({ auth })`.
10
+ // 3. Mount Better Auth's handler at `app/api/auth/[...all]/route.ts`.
11
+
12
+ import { createRaredaysApp, definePermissions, Session, User } from "@raredays/core";
13
+ import { createInMemoryProvider } from "@raredays/identity/testing";
14
+ // CRDA:imports
15
+
16
+ // — Authorization (what each role can do) ─────────────────────────────────
17
+ definePermissions({
18
+ rolePermissions: {
19
+ member: ["content.read"],
20
+ admin: ["content.read", "content.manage"],
21
+ },
22
+ });
23
+
24
+ // — Seeded dev user (replace with Better Auth in production) ──────────────
25
+ const ALICE = User.from({
26
+ id: "u_alice",
27
+ email: "alice@example.com",
28
+ organizationId: "default",
29
+ roles: ["member"],
30
+ emailVerifiedAt: new Date("2026-01-01T00:00:00Z"),
31
+ });
32
+
33
+ const ALICE_SESSION = Session.from({
34
+ id: "s_alice",
35
+ userId: ALICE.id,
36
+ user: ALICE.toJSON(),
37
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
38
+ });
39
+
40
+ const identityProvider = createInMemoryProvider({
41
+ users: [ALICE],
42
+ sessions: [ALICE_SESSION],
43
+ });
44
+
45
+ export const SEEDED = { user: ALICE, sessionId: ALICE_SESSION.id };
46
+
47
+ // CRDA:config
48
+
49
+ export const app = createRaredaysApp({
50
+ identityProvider,
51
+ // CRDA:ports
52
+ });
53
+
54
+ // CRDA:exports
@@ -0,0 +1,31 @@
1
+ import { redirect } from "next/navigation";
2
+ import { UnauthenticatedError } from "@raredays/core";
3
+ import { getServerSession } from "@raredays/identity/server";
4
+ import { app } from "@/lib/runtime";
5
+
6
+ export default async function ProtectedPage(): Promise<React.ReactElement> {
7
+ const session = await getServerSession(app);
8
+ if (!session) {
9
+ redirect("/");
10
+ }
11
+
12
+ try {
13
+ // Framework-owned authz: gate on a permission, not a role string.
14
+ session.user.requirePermission("content.read");
15
+ } catch (err) {
16
+ if (err instanceof UnauthenticatedError) redirect("/");
17
+ throw err;
18
+ }
19
+
20
+ return (
21
+ <main data-testid="protected" className="p-8 font-sans">
22
+ <h1 className="text-2xl font-bold">Protected</h1>
23
+ <p className="mt-3" data-testid="signed-in-as">
24
+ Signed in as {session.user.email}
25
+ </p>
26
+ <p className="mt-1 text-sm text-neutral-600" data-testid="user-id">
27
+ User id: {session.user.id}
28
+ </p>
29
+ </main>
30
+ );
31
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Auto-generated by @raredays/theme-build. Do not edit by hand.
3
+ * Source: site.spec.ts
4
+ * To regenerate: pnpm theme:build
5
+ */
6
+
7
+ @theme {
8
+ /* No spec overrides — defaults from @raredays/tokens apply. */
9
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Validate that pending migrations apply cleanly without committing changes.
3
+ * Invoked by `pnpm db:check` — designed for CI pull-request gates.
4
+ *
5
+ * Strategy: redirect Drizzle's bookkeeping table to a scratch schema, run
6
+ * the migrations against it, then `DROP SCHEMA ... CASCADE`. The check
7
+ * verifies the migration SQL parses + the bookkeeping plumbing works. For
8
+ * full destructive-change preview (does the SQL still apply against the
9
+ * current production data?), run against an ephemeral clone separately.
10
+ *
11
+ * Uses PGlite when `DATABASE_URL` is unset so CI runners can validate
12
+ * migrations without provisioning Postgres.
13
+ */
14
+ import { resolve } from "node:path";
15
+ import { dryRun } from "@raredays/postgres/migrations";
16
+ import * as appSchema from "./schema.js";
17
+
18
+ const migrationsFolder = resolve(import.meta.dirname, "migrations");
19
+ const databaseUrl = process.env.DATABASE_URL;
20
+
21
+ async function main(): Promise<void> {
22
+ if (databaseUrl) {
23
+ const { createDb } = await import("@raredays/postgres-neon");
24
+ const { migrate } = await import("drizzle-orm/neon-http/migrator");
25
+ const db = createDb(databaseUrl, { schema: appSchema });
26
+ await dryRun({ db, migrate, migrationsFolder });
27
+ return;
28
+ }
29
+
30
+ console.warn(
31
+ "[db:check] DATABASE_URL is not set — validating against in-process PGlite.",
32
+ );
33
+ const { createPglite } = await import("@raredays/postgres-pglite");
34
+ const { migrate } = await import("drizzle-orm/pglite/migrator");
35
+ const handle = await createPglite({ schema: appSchema });
36
+ try {
37
+ await dryRun({ db: handle.db, migrate, migrationsFolder });
38
+ } finally {
39
+ await handle.close();
40
+ }
41
+ }
42
+
43
+ main().catch((err) => {
44
+ console.error("[db:check] failed:", err);
45
+ process.exit(1);
46
+ });
@@ -0,0 +1,109 @@
1
+ // Database client — branches on environment.
2
+ //
3
+ // Production path: `@raredays/postgres-neon` (HTTP, edge-runtime
4
+ // compatible). Set `DATABASE_URL` and the lazy Proxy below resolves to a
5
+ // Neon client on first property access. To swap providers (Supabase /
6
+ // RDS / self-hosted via `@raredays/postgres-js`, or any other
7
+ // `@raredays/postgres-*` driver):
8
+ //
9
+ // 1. Edit `site.spec.ts` and set `deploy.database.provider` accordingly.
10
+ // 2. `pnpm remove @raredays/postgres-neon && pnpm add @raredays/postgres-js`
11
+ // 3. Replace the `@raredays/postgres-neon` static import below.
12
+ //
13
+ // Dev path (NODE_ENV !== "production" AND no DATABASE_URL): a fresh
14
+ // in-process `@raredays/postgres-pglite` is booted eagerly so
15
+ // Drizzle-backed providers (state-machine, engagement, audit, future
16
+ // cms-drizzle) can register their tables at composition root without
17
+ // provisioning Postgres. PGlite resets every time the dev server
18
+ // restarts; set `DATABASE_URL` to opt into a persistent database in dev.
19
+ //
20
+ // Why eager in dev and lazy in prod? Drizzle providers materialize
21
+ // tables at `registerMachine` / `registerTable` time — i.e. during
22
+ // composition root eval. The dev runtime needs the db handle ready
23
+ // before `createRaredaysApp` is called. The prod runtime can stay
24
+ // lazy: the Neon client is cheap to construct, and lazy construction
25
+ // means a build that never queries (e.g. SSG-only marketing routes)
26
+ // doesn't require DATABASE_URL.
27
+ import { createDb, type RaredaysDatabase as NeonDb } from "@raredays/postgres-neon";
28
+ import { createPglite, type RaredaysDatabase as PgliteDb } from "@raredays/postgres-pglite";
29
+ import * as appSchema from "./schema.js";
30
+
31
+ export type Db = NeonDb<typeof appSchema> | PgliteDb<typeof appSchema>;
32
+
33
+ const isProduction = process.env.NODE_ENV === "production";
34
+ const hasDatabaseUrl = Boolean(process.env.DATABASE_URL);
35
+
36
+ // Dev fallback: bootstrap PGlite eagerly so providers can register
37
+ // tables at composition-root eval. In prod, or when DATABASE_URL is
38
+ // set, leave this null and fall through to the lazy Proxy below.
39
+ const eagerPglite: Db | null =
40
+ !isProduction && !hasDatabaseUrl ? await bootstrapPglite() : null;
41
+
42
+ let cached: Db | undefined = eagerPglite ?? undefined;
43
+
44
+ function buildNeon(): Db {
45
+ const connectionString = process.env.DATABASE_URL;
46
+ if (!connectionString) {
47
+ throw new Error(
48
+ "DATABASE_URL is not set. Provision a Postgres database (Neon, Supabase, etc.) and add it to the environment before running queries.",
49
+ );
50
+ }
51
+ return createDb(connectionString, { schema: appSchema }) as Db;
52
+ }
53
+
54
+ async function bootstrapPglite(): Promise<Db> {
55
+ const handle = await createPglite({ schema: appSchema });
56
+ // Minimal CREATE TABLE statements mirroring the schemas exported by
57
+ // `@raredays/postgres`. Kept tight on purpose — Drizzle-provider tests
58
+ // exercise the real conformance suite; this is purely so a fresh
59
+ // `pnpm dev` doesn't crash on first query against substrate tables. If
60
+ // you need the full migration set in dev, point DATABASE_URL at a
61
+ // local Postgres and run `pnpm db:migrate`.
62
+ const statements: readonly string[] = [
63
+ `CREATE TABLE IF NOT EXISTS audit_events (
64
+ id text PRIMARY KEY,
65
+ organization_id text NOT NULL,
66
+ actor_id text,
67
+ action text NOT NULL,
68
+ resource_type text NOT NULL,
69
+ resource_id text,
70
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
71
+ created_at timestamptz NOT NULL DEFAULT now()
72
+ )`,
73
+ `CREATE TABLE IF NOT EXISTS webhook_deliveries (
74
+ id text PRIMARY KEY,
75
+ organization_id text NOT NULL,
76
+ endpoint text NOT NULL,
77
+ event_type text NOT NULL,
78
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
79
+ status text NOT NULL DEFAULT 'pending',
80
+ attempts integer NOT NULL DEFAULT 0,
81
+ last_error text,
82
+ created_at timestamptz NOT NULL DEFAULT now(),
83
+ updated_at timestamptz NOT NULL DEFAULT now()
84
+ )`,
85
+ `CREATE TABLE IF NOT EXISTS job_executions (
86
+ id text PRIMARY KEY,
87
+ organization_id text NOT NULL,
88
+ job_name text NOT NULL,
89
+ status text NOT NULL DEFAULT 'pending',
90
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
91
+ result jsonb,
92
+ error text,
93
+ scheduled_at timestamptz NOT NULL DEFAULT now(),
94
+ started_at timestamptz,
95
+ completed_at timestamptz
96
+ )`,
97
+ ];
98
+ for (const stmt of statements) {
99
+ await handle.client.query(stmt);
100
+ }
101
+ return handle.db as Db;
102
+ }
103
+
104
+ export const db: Db = new Proxy({} as Db, {
105
+ get(_target, prop) {
106
+ if (!cached) cached = buildNeon();
107
+ return Reflect.get(cached, prop);
108
+ },
109
+ }) as Db;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Apply pending Drizzle migrations against the configured Postgres provider.
3
+ *
4
+ * Invoked by `pnpm db:migrate`. Branches on environment:
5
+ *
6
+ * - **Production / staging** (`DATABASE_URL` set, NODE_ENV=production): uses
7
+ * `@raredays/postgres-neon` by default. Swap the import if you've migrated
8
+ * to `@raredays/postgres-js`.
9
+ * - **Dev with persistent Postgres** (`DATABASE_URL` set, any NODE_ENV):
10
+ * same path as production — connects to the URL.
11
+ * - **Dev fallback** (no `DATABASE_URL`): boots PGlite in-process so the
12
+ * command always exits cleanly. PGlite is ephemeral, so migrations against
13
+ * it only validate that the SQL parses; nothing is persisted.
14
+ *
15
+ * Wraps Drizzle's `migrate()` with `@raredays/postgres/migrations`'s
16
+ * `safeMigrate` to acquire an advisory lock (preventing concurrent
17
+ * migration runs) and emit structured logs.
18
+ *
19
+ * Exit codes: 0 on success, 1 on any failure.
20
+ */
21
+ import { resolve } from "node:path";
22
+ import { safeMigrate } from "@raredays/postgres/migrations";
23
+ import * as appSchema from "./schema.js";
24
+
25
+ const migrationsFolder = resolve(import.meta.dirname, "migrations");
26
+ const appKey = process.env.npm_package_name ?? "raredays-app";
27
+ const databaseUrl = process.env.DATABASE_URL;
28
+
29
+ async function main(): Promise<void> {
30
+ if (databaseUrl) {
31
+ // Neon HTTP path. If you've swapped to @raredays/postgres-js, change
32
+ // both imports below to match.
33
+ const { createDb } = await import("@raredays/postgres-neon");
34
+ const { migrate } = await import("drizzle-orm/neon-http/migrator");
35
+ const db = createDb(databaseUrl, { schema: appSchema });
36
+ await safeMigrate({ db, migrate, migrationsFolder, appKey });
37
+ return;
38
+ }
39
+
40
+ // Dev fallback: PGlite in-process. Migrations apply against an ephemeral
41
+ // database so this command never fails for missing config.
42
+ console.warn(
43
+ "[db:migrate] DATABASE_URL is not set — falling back to in-process PGlite. " +
44
+ "Set DATABASE_URL to persist migrations.",
45
+ );
46
+ const { createPglite } = await import("@raredays/postgres-pglite");
47
+ const { migrate } = await import("drizzle-orm/pglite/migrator");
48
+ const handle = await createPglite({ schema: appSchema });
49
+ try {
50
+ await safeMigrate({
51
+ db: handle.db,
52
+ migrate,
53
+ migrationsFolder,
54
+ appKey,
55
+ // PGlite supports advisory locks but they're meaningless in-process.
56
+ useAdvisoryLock: false,
57
+ });
58
+ } finally {
59
+ await handle.close();
60
+ }
61
+ }
62
+
63
+ main().catch((err) => {
64
+ console.error("[db:migrate] failed:", err);
65
+ process.exit(1);
66
+ });
File without changes
@@ -0,0 +1,20 @@
1
+ /**
2
+ * App-side schema barrel. Re-exports the framework's substrate tables and is
3
+ * the place to declare app-specific tables. The drizzle-kit config in
4
+ * `drizzle.config.ts` points at this file, so any table declared here gets
5
+ * generated into a migration when you run `pnpm db:generate`.
6
+ */
7
+ export * from "@raredays/postgres/schema";
8
+
9
+ // Declare app-specific tables here. Use the column helpers from
10
+ // @raredays/postgres so every table stays multi-tenancy-ready. Example:
11
+ //
12
+ // import { pgTable, text, uuid } from "drizzle-orm/pg-core";
13
+ // import { tenantColumns, timestampColumns } from "@raredays/postgres";
14
+ //
15
+ // export const widgets = pgTable("widgets", {
16
+ // id: uuid("id").primaryKey().defaultRandom(),
17
+ // ...tenantColumns(),
18
+ // ...timestampColumns(),
19
+ // name: text("name").notNull(),
20
+ // });
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ // `drizzle-kit generate` reads the schema only and does NOT need a
4
+ // connection string; `drizzle-kit push` / `studio` / `migrate` do. We
5
+ // therefore allow the URL to be absent at config eval time and let the
6
+ // individual drizzle-kit commands surface their own clearer errors.
7
+ const url = process.env.DATABASE_URL ?? "postgres://placeholder@localhost:5432/placeholder";
8
+
9
+ export default defineConfig({
10
+ dialect: "postgresql",
11
+ schema: "./db/schema.ts",
12
+ out: "./db/migrations",
13
+ dbCredentials: { url },
14
+ strict: true,
15
+ verbose: true,
16
+ });
@@ -0,0 +1,10 @@
1
+ // Next.js calls `register()` early in the server boot, before any route
2
+ // handler runs. The Node-runtime guard avoids starting MSW in edge
3
+ // runtimes where setupServer isn't compatible.
4
+
5
+ export async function register(): Promise<void> {
6
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
7
+ if (process.env.MOCKS !== "on") return;
8
+ const { mockServer } = await import("./mocks/node");
9
+ mockServer.start();
10
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ // Browser-side mock worker. Wraps the app so that no in-page fetch fires
4
+ // until the Service Worker has claimed the page — otherwise the very
5
+ // first request races the SW registration and slips through unmocked.
6
+ //
7
+ // When `NEXT_PUBLIC_MOCKS` is unset (production), the wrapper renders
8
+ // children synchronously with zero overhead. Requires
9
+ // `public/mockServiceWorker.js` — shipped by the scaffold and pinned
10
+ // to the same `msw` version as this template. Regenerate with
11
+ // `npx msw init public/ --save` only if you bump the `msw` dep.
12
+
13
+ import { useEffect, useState, type ReactNode } from "react";
14
+ import { enableMockWorker } from "@raredays/mocks/browser";
15
+ import { handlers } from "./handlers";
16
+
17
+ const ENABLED = process.env.NEXT_PUBLIC_MOCKS === "on";
18
+ const worker = ENABLED ? enableMockWorker(handlers) : null;
19
+
20
+ export function StartMocks({ children }: { children: ReactNode }): ReactNode {
21
+ const [ready, setReady] = useState(!ENABLED);
22
+ useEffect(() => {
23
+ if (!worker) return;
24
+ void worker.start().then(() => setReady(true));
25
+ return () => worker.stop();
26
+ }, []);
27
+ return ready ? children : null;
28
+ }
@@ -0,0 +1,34 @@
1
+ // Mock composition root. Imported by `mocks/node.ts`, `mocks/browser.tsx`,
2
+ // and the vitest setup file — same handlers, three environments.
3
+ //
4
+ // To enable per-vendor canonical mocks, install the corresponding
5
+ // `@raredays/*` packages (already in your deps if you selected the module)
6
+ // and uncomment the relevant import + spread below.
7
+ //
8
+ // Per-app handlers go in the trailing array; later handlers win on equal
9
+ // URL specificity, so app-specific overrides should come after vendor sets.
10
+
11
+ import { defineMocks, type RequestHandler } from "@raredays/mocks";
12
+ import { http, HttpResponse } from "msw";
13
+ // CRDA:mocks-imports
14
+
15
+ // Explicit annotation — TS2742 otherwise points at MSW's internal
16
+ // HttpResponse-*.mjs path which isn't portable across consumer apps.
17
+ export const handlers: RequestHandler[] = defineMocks(
18
+ // CRDA:mocks-spread
19
+ [
20
+ // App-local handlers — the demo at /demo/widgets calls /api/widgets,
21
+ // which by default returns []. With MOCKS on, MSW intercepts here and
22
+ // returns canned data so you can design UI against realistic responses.
23
+ http.get("/api/widgets", () =>
24
+ HttpResponse.json({
25
+ widgets: [
26
+ { id: "w_1", name: "Mock Widget A", inventory: 12 },
27
+ { id: "w_2", name: "Mock Widget B", inventory: 7 },
28
+ { id: "w_3", name: "Mock Widget C", inventory: 0 },
29
+ ],
30
+ total: 3,
31
+ }),
32
+ ),
33
+ ],
34
+ );