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,104 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ # Mirrors the RareDays framework's CI discipline for consumer apps.
14
+ # Client template has no backend / db steps; otherwise identical
15
+ # verification chain to the full-stack template.
16
+
17
+ jobs:
18
+ lint:
19
+ name: Lint
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 10
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: pnpm/action-setup@v4
25
+ - uses: actions/setup-node@v4
26
+ with: { node-version: 24, cache: pnpm }
27
+ - run: pnpm install --frozen-lockfile
28
+ - run: pnpm lint
29
+
30
+ format-check:
31
+ name: Format check
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 10
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - uses: pnpm/action-setup@v4
37
+ - uses: actions/setup-node@v4
38
+ with: { node-version: 24, cache: pnpm }
39
+ - run: pnpm install --frozen-lockfile
40
+ - run: pnpm format:check
41
+
42
+ style:
43
+ name: Style
44
+ runs-on: ubuntu-latest
45
+ timeout-minutes: 10
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ - uses: pnpm/action-setup@v4
49
+ - uses: actions/setup-node@v4
50
+ with: { node-version: 24, cache: pnpm }
51
+ - run: pnpm install --frozen-lockfile
52
+ - run: pnpm style
53
+
54
+ typecheck:
55
+ name: Typecheck
56
+ runs-on: ubuntu-latest
57
+ timeout-minutes: 10
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ - uses: pnpm/action-setup@v4
61
+ - uses: actions/setup-node@v4
62
+ with: { node-version: 24, cache: pnpm }
63
+ - run: pnpm install --frozen-lockfile
64
+ - run: pnpm theme:build
65
+ - run: pnpm typecheck
66
+
67
+ test:
68
+ name: Test
69
+ runs-on: ubuntu-latest
70
+ timeout-minutes: 15
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: pnpm/action-setup@v4
74
+ - uses: actions/setup-node@v4
75
+ with: { node-version: 24, cache: pnpm }
76
+ - run: pnpm install --frozen-lockfile
77
+ - run: pnpm theme:build
78
+ - run: pnpm test
79
+
80
+ build:
81
+ name: Build
82
+ runs-on: ubuntu-latest
83
+ timeout-minutes: 15
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ - uses: pnpm/action-setup@v4
87
+ - uses: actions/setup-node@v4
88
+ with: { node-version: 24, cache: pnpm }
89
+ - run: pnpm install --frozen-lockfile
90
+ - run: pnpm build
91
+
92
+ storybook:
93
+ name: Storybook build
94
+ runs-on: ubuntu-latest
95
+ timeout-minutes: 15
96
+ if: hashFiles('.storybook/main.ts', '.storybook/main.js') != ''
97
+ steps:
98
+ - uses: actions/checkout@v4
99
+ - uses: pnpm/action-setup@v4
100
+ - uses: actions/setup-node@v4
101
+ with: { node-version: 24, cache: pnpm }
102
+ - run: pnpm install --frozen-lockfile
103
+ - run: pnpm theme:build
104
+ - run: pnpm storybook:build
@@ -0,0 +1,15 @@
1
+ node_modules/
2
+ .next/
3
+ out/
4
+ dist/
5
+ *.tsbuildinfo
6
+ next-env.d.ts
7
+
8
+ .env
9
+ .env.local
10
+ .env.*.local
11
+
12
+ .skillex/
13
+
14
+ .DS_Store
15
+ *.log
@@ -0,0 +1 @@
1
+ @raredays:registry=http://localhost:4873
@@ -0,0 +1,49 @@
1
+ import {
2
+ AppLayout,
3
+ AppTopBar,
4
+ Sidebar,
5
+ SidebarContent,
6
+ SidebarGroup,
7
+ SidebarGroupLabel,
8
+ SidebarGroupContent,
9
+ SidebarMenu,
10
+ SidebarMenuItem,
11
+ SidebarMenuButton,
12
+ SidebarHeader,
13
+ } from "@raredays/components";
14
+ import { ShieldIcon } from "lucide-react";
15
+ import type { ReactNode } from "react";
16
+
17
+ export default function AppGroupLayout({ children }: { children: ReactNode }) {
18
+ return (
19
+ <AppLayout
20
+ sidebar={
21
+ <Sidebar>
22
+ <SidebarHeader>
23
+ <span className="px-2 text-lg font-bold">RareDays</span>
24
+ </SidebarHeader>
25
+ <SidebarContent>
26
+ <SidebarGroup>
27
+ <SidebarGroupLabel>App</SidebarGroupLabel>
28
+ <SidebarGroupContent>
29
+ <SidebarMenu>
30
+ <SidebarMenuItem>
31
+ <SidebarMenuButton asChild>
32
+ <a href="/protected">
33
+ <ShieldIcon />
34
+ <span>Protected</span>
35
+ </a>
36
+ </SidebarMenuButton>
37
+ </SidebarMenuItem>
38
+ </SidebarMenu>
39
+ </SidebarGroupContent>
40
+ </SidebarGroup>
41
+ </SidebarContent>
42
+ </Sidebar>
43
+ }
44
+ topbar={<AppTopBar />}
45
+ >
46
+ {children}
47
+ </AppLayout>
48
+ );
49
+ }
@@ -0,0 +1,21 @@
1
+ import { MarketingLayout, MarketingHeader, MarketingFooter } from "@raredays/components";
2
+ import type { ReactNode } from "react";
3
+
4
+ export default function MarketingGroupLayout({ children }: { children: ReactNode }) {
5
+ return (
6
+ <MarketingLayout
7
+ header={
8
+ <MarketingHeader
9
+ logo={<span className="text-lg font-bold">RareDays</span>}
10
+ navigation={[
11
+ { label: "Features", href: "/#features" },
12
+ { label: "Pricing", href: "/#pricing" },
13
+ ]}
14
+ />
15
+ }
16
+ footer={<MarketingFooter copyright={`© ${new Date().getFullYear()} RareDays`} />}
17
+ >
18
+ {children}
19
+ </MarketingLayout>
20
+ );
21
+ }
@@ -0,0 +1,34 @@
1
+ import { HomeAuthControls } from "../home-auth-controls";
2
+
3
+ export default function HomePage(): React.ReactElement {
4
+ return (
5
+ <main className="mx-auto max-w-2xl p-8 font-sans">
6
+ <h1 className="text-3xl font-bold">RareDays — BFF + Client</h1>
7
+ <p className="mt-4 text-neutral-600">
8
+ Next.js BFF that authenticates users and proxies API calls to an external backend. Identity
9
+ lives here; data lives in the backend. Configure <code>BACKEND_URL</code> in your env to
10
+ point at the upstream service.
11
+ </p>
12
+ <HomeAuthControls />
13
+ <h2 className="mt-10 text-lg font-semibold">Next steps</h2>
14
+ <ul className="mt-3 list-disc space-y-1 pl-5 text-sm text-neutral-700">
15
+ <li>
16
+ Edit <code>app/lib/backend-client.ts</code> to customise headers, retries, observability.
17
+ </li>
18
+ <li>
19
+ Edit <code>app/api/backend/[...path]/route.ts</code> if your backend needs different
20
+ request shaping or response filtering.
21
+ </li>
22
+ <li>
23
+ The proxy forwards <code>x-rd-user-id</code>, <code>x-rd-organization-id</code>, and{" "}
24
+ <code>x-rd-roles</code> headers — your backend reads these instead of resolving auth
25
+ itself.
26
+ </li>
27
+ <li>
28
+ Run <code>pnpm test</code> to execute the sample test that exercises the in-memory
29
+ identity provider.
30
+ </li>
31
+ </ul>
32
+ </main>
33
+ );
34
+ }
@@ -0,0 +1,37 @@
1
+ // BFF proxy. Any request the SPA makes to /api/backend/<path> is forwarded
2
+ // to the configured backend with the authenticated user's context attached.
3
+ //
4
+ // Customise: pull session via `app.require("identityProvider").getSession(req)`
5
+ // and inject headers (user id, roles, tenant) before forwarding. Treat the
6
+ // backend as the source of truth; this layer is just routing + identity.
7
+
8
+ import { proxyToBackend } from "@/lib/backend-client";
9
+ import { app } from "@/lib/runtime";
10
+
11
+ async function forward(req: Request, { params }: { params: Promise<{ path: string[] }> }) {
12
+ const { path } = await params;
13
+ const session = await app.require("identityProvider").getSession(req);
14
+
15
+ // Strip hop-by-hop headers + the host header (would point at the BFF, not
16
+ // the backend); pass everything else through.
17
+ const outboundHeaders = new Headers(req.headers);
18
+ outboundHeaders.delete("host");
19
+ outboundHeaders.delete("connection");
20
+ if (session) {
21
+ outboundHeaders.set("x-rd-user-id", session.user.id);
22
+ outboundHeaders.set("x-rd-organization-id", session.user.organizationId);
23
+ outboundHeaders.set("x-rd-roles", session.user.roles.join(","));
24
+ }
25
+
26
+ return proxyToBackend(path.join("/"), {
27
+ method: req.method,
28
+ headers: outboundHeaders,
29
+ body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
30
+ });
31
+ }
32
+
33
+ export const GET = forward;
34
+ export const POST = forward;
35
+ export const PUT = forward;
36
+ export const PATCH = forward;
37
+ export const DELETE = forward;
@@ -0,0 +1,14 @@
1
+ import { NextResponse } from "next/server";
2
+ import { SEEDED } from "@/lib/runtime";
3
+
4
+ // Dev-only: drop a cookie matching the seeded session. In production this is
5
+ // replaced by Better Auth's sign-in flow at /api/auth/*.
6
+ export async function POST(): Promise<NextResponse> {
7
+ const res = NextResponse.json({ ok: true, userId: SEEDED.user.id });
8
+ res.cookies.set("rd-dev-session", SEEDED.sessionId, {
9
+ httpOnly: true,
10
+ sameSite: "lax",
11
+ path: "/",
12
+ });
13
+ return res;
14
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse, type NextRequest } from "next/server";
2
+
3
+ export async function POST(request: NextRequest): Promise<NextResponse> {
4
+ // Progressive enhancement: an HTML form post (Accept: text/html) gets a
5
+ // 303 redirect back to /sign-in; a fetch() call (Accept: application/json)
6
+ // gets the original JSON response.
7
+ const wantsHtml = request.headers.get("accept")?.includes("text/html") ?? false;
8
+
9
+ if (wantsHtml) {
10
+ const res = NextResponse.redirect(new URL("/sign-in", request.url), 303);
11
+ res.cookies.delete("rd-dev-session");
12
+ return res;
13
+ }
14
+
15
+ const res = NextResponse.json({ ok: true });
16
+ res.cookies.delete("rd-dev-session");
17
+ return res;
18
+ }
@@ -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,55 @@
1
+ // BFF → external backend wiring. Two surfaces, one upstream:
2
+ //
3
+ // • `backend` — typed JSON client (`backend.get<T>("/widgets")`). Use
4
+ // this from server components, route handlers, and any code that
5
+ // calls the backend with a typed response.
6
+ //
7
+ // • `proxyToBackend` — raw `Response` passthrough used by the
8
+ // `app/api/backend/[...path]/route.ts` proxy. Necessary because the
9
+ // proxy streams the body untouched; the typed client buffers it.
10
+ //
11
+ // Configure `BACKEND_URL` in your env. Tokens, retry/timeout, and auth
12
+ // header injection live in this file — that's the one place that talks
13
+ // to the upstream service.
14
+
15
+ import { createFetchHttpClient, type HttpClient } from "@raredays/core";
16
+
17
+ const BACKEND_URL = process.env.BACKEND_URL ?? "";
18
+
19
+ // Lazy-initialized: warn once, on first actual use of the backend client,
20
+ // rather than at module-import time. This keeps `next build` quiet for
21
+ // apps that don't (yet) use backend calls, while still surfacing a clear
22
+ // signal the first time a missing config bites.
23
+ let warnedMissingBackendUrl = false;
24
+ function warnIfMissingBackendUrl(): void {
25
+ if (warnedMissingBackendUrl) return;
26
+ if (BACKEND_URL) return;
27
+ if (process.env.NODE_ENV !== "production") return;
28
+ warnedMissingBackendUrl = true;
29
+ // eslint-disable-next-line no-console
30
+ console.warn("[backend-client] BACKEND_URL is not set — backend calls will fail.");
31
+ }
32
+
33
+ export const backend: HttpClient = createFetchHttpClient({
34
+ // `baseUrl` is read at call time by the underlying client, so the lazy
35
+ // warning above is sufficient — no module-load side effect needed.
36
+ baseUrl: BACKEND_URL,
37
+ // Default retry: 2 attempts on 5xx/network errors with 100ms backoff.
38
+ // Tune per-call via `backend.get(path, { retry: { ... } })` when you need
39
+ // different semantics (e.g. webhooks shouldn't retry, batch jobs should).
40
+ retry: { maxAttempts: 2 },
41
+ });
42
+
43
+ /**
44
+ * Raw passthrough for the `[...path]` proxy route. Keeps the streaming
45
+ * semantics that `fetch` provides — the typed `backend` client buffers
46
+ * the body and isn't appropriate for proxying.
47
+ */
48
+ export async function proxyToBackend(path: string, init: RequestInit): Promise<Response> {
49
+ warnIfMissingBackendUrl();
50
+ if (!BACKEND_URL) {
51
+ return new Response("BACKEND_URL not configured", { status: 502 });
52
+ }
53
+ const url = `${BACKEND_URL.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
54
+ return fetch(url, init);
55
+ }
@@ -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 for the BFF layer. This Next.js app sits in front of
2
+ // your real backend (Rails, Django, Go, Java, etc.) and handles:
3
+ // - authentication / session resolution
4
+ // - request proxying with auth context attached (see app/api/backend/[...path])
5
+ // - any client-side state / SSR shell the SPA needs
6
+ //
7
+ // Business logic (billing, persistence, notifications) lives in the backend.
8
+ // Identity is the one port the BFF owns. Default uses the in-memory provider
9
+ // so `pnpm dev` runs out of the box; swap to `@raredays/identity-better-auth`
10
+ // in production (and mount its 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,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,20 @@
1
+ // Mock composition root. Imported by `mocks/node.ts`, `mocks/browser.tsx`,
2
+ // and the vitest setup file — same handlers, three environments.
3
+ //
4
+ // In a client/BFF app, the most common pattern is intercepting fetches to
5
+ // the upstream backend (proxied through `/api/backend/*`). Add those
6
+ // handlers to the trailing array below.
7
+
8
+ import { defineMocks, type RequestHandler } from "@raredays/mocks";
9
+ // import { http, HttpResponse } from "msw";
10
+ // CRDA:mocks-imports
11
+
12
+ // Explicit annotation — TS2742 otherwise points at MSW's internal
13
+ // HttpResponse-*.mjs path which isn't portable across consumer apps.
14
+ export const handlers: RequestHandler[] = defineMocks(
15
+ // CRDA:mocks-spread
16
+ [
17
+ // App-local handlers, e.g.:
18
+ // http.get("/api/backend/products", () => HttpResponse.json({ products: [] })),
19
+ ],
20
+ );
@@ -0,0 +1,7 @@
1
+ // Node-side mock server. Activated by `instrumentation.ts` when
2
+ // `MOCKS=on`, and by `vitest.setup.ts` during tests.
3
+
4
+ import { setupMockServer } from "@raredays/mocks/node";
5
+ import { handlers } from "./handlers";
6
+
7
+ export const mockServer = setupMockServer(handlers);
@@ -0,0 +1,8 @@
1
+ import { withRaredays } from "@raredays/core";
2
+ import type { NextConfig } from "next";
3
+
4
+ const config: NextConfig = withRaredays({
5
+ // Project-specific Next.js config goes here.
6
+ });
7
+
8
+ export default config;