@forgeailab/create-spark 0.1.1 → 0.1.3

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 (170) hide show
  1. package/.claude/skills/architecture-cutline/SKILL.md +96 -0
  2. package/.claude/skills/board-review/SKILL.md +77 -0
  3. package/.claude/skills/code-review/SKILL.md +76 -0
  4. package/.claude/skills/execute-task/SKILL.md +80 -0
  5. package/.claude/skills/idea-sharpen/SKILL.md +65 -0
  6. package/.claude/skills/implementation-brief/SKILL.md +87 -0
  7. package/.claude/skills/mvp-board/SKILL.md +95 -0
  8. package/.claude/skills/mvp-grill/SKILL.md +60 -0
  9. package/.claude/skills/mvp-spec/SKILL.md +78 -0
  10. package/.claude/skills/new-pack/SKILL.md +156 -0
  11. package/.claude/skills/next-task/SKILL.md +65 -0
  12. package/.claude/skills/pack-add/SKILL.md +64 -0
  13. package/.claude/skills/pack-resolve/SKILL.md +67 -0
  14. package/.claude/skills/parallel-execution/SKILL.md +68 -0
  15. package/.claude/skills/qa-verify/SKILL.md +77 -0
  16. package/.claude/skills/risk-check/SKILL.md +88 -0
  17. package/.claude/skills/sync-board/SKILL.md +76 -0
  18. package/.claude/skills/ux-theme/SKILL.md +93 -0
  19. package/.codex/skills/architecture-cutline/SKILL.md +94 -0
  20. package/.codex/skills/board-review/SKILL.md +75 -0
  21. package/.codex/skills/code-review/SKILL.md +73 -0
  22. package/.codex/skills/execute-task/SKILL.md +76 -0
  23. package/.codex/skills/idea-sharpen/SKILL.md +63 -0
  24. package/.codex/skills/implementation-brief/SKILL.md +85 -0
  25. package/.codex/skills/mvp-board/SKILL.md +93 -0
  26. package/.codex/skills/mvp-grill/SKILL.md +58 -0
  27. package/.codex/skills/mvp-spec/SKILL.md +76 -0
  28. package/.codex/skills/new-pack/SKILL.md +153 -0
  29. package/.codex/skills/next-task/SKILL.md +64 -0
  30. package/.codex/skills/pack-add/SKILL.md +62 -0
  31. package/.codex/skills/pack-resolve/SKILL.md +65 -0
  32. package/.codex/skills/parallel-execution/SKILL.md +66 -0
  33. package/.codex/skills/qa-verify/SKILL.md +74 -0
  34. package/.codex/skills/risk-check/SKILL.md +86 -0
  35. package/.codex/skills/sync-board/SKILL.md +72 -0
  36. package/.codex/skills/ux-theme/SKILL.md +91 -0
  37. package/package.json +10 -4
  38. package/packs/README.md +132 -0
  39. package/packs/ai-anthropic/files/app/api/ai/route.ts +57 -0
  40. package/packs/ai-anthropic/files/lib/anthropic.ts +15 -0
  41. package/packs/ai-anthropic/pack.toml +32 -0
  42. package/packs/ai-anthropic/skills/ai-feature-patterns/SKILL.md +87 -0
  43. package/packs/ai-anthropic/tasks.yaml +9 -0
  44. package/packs/ai-openai/files/app/api/ai-openai/route.ts +55 -0
  45. package/packs/ai-openai/files/lib/openai.ts +21 -0
  46. package/packs/ai-openai/pack.toml +30 -0
  47. package/packs/ai-openai/tasks.yaml +9 -0
  48. package/packs/analytics-posthog/files/components/PostHogProvider.tsx +19 -0
  49. package/packs/analytics-posthog/files/lib/posthog/client.ts +20 -0
  50. package/packs/analytics-posthog/files/lib/posthog/server.ts +24 -0
  51. package/packs/analytics-posthog/pack.toml +35 -0
  52. package/packs/analytics-posthog/tasks.yaml +15 -0
  53. package/packs/auth-better-auth/files/app/(auth)/login/page.tsx +58 -0
  54. package/packs/auth-better-auth/files/app/api/auth/[...all]/route.ts +4 -0
  55. package/packs/auth-better-auth/files/lib/auth.ts +21 -0
  56. package/packs/auth-better-auth/pack.toml +32 -0
  57. package/packs/auth-better-auth/tasks.yaml +10 -0
  58. package/packs/auth-better-auth-pg/files/app/api/auth/[...all]/route.ts +4 -0
  59. package/packs/auth-better-auth-pg/files/lib/auth.ts +86 -0
  60. package/packs/auth-better-auth-pg/pack.toml +32 -0
  61. package/packs/auth-better-auth-pg/tasks.yaml +17 -0
  62. package/packs/auth-supabase/files/app/(auth)/login/page.tsx +64 -0
  63. package/packs/auth-supabase/files/app/auth/callback/route.ts +15 -0
  64. package/packs/auth-supabase/files/middleware.ts +41 -0
  65. package/packs/auth-supabase/pack.toml +34 -0
  66. package/packs/auth-supabase/tasks.yaml +10 -0
  67. package/packs/db-postgres/files/compose/postgres.yml +28 -0
  68. package/packs/db-postgres/files/docker-compose.include.yml +1 -0
  69. package/packs/db-postgres/files/docker-compose.yml +6 -0
  70. package/packs/db-postgres/files/drizzle.config.ts +10 -0
  71. package/packs/db-postgres/files/lib/db/index.ts +10 -0
  72. package/packs/db-postgres/files/lib/db/schema.ts +11 -0
  73. package/packs/db-postgres/pack.toml +53 -0
  74. package/packs/db-postgres/tasks.yaml +11 -0
  75. package/packs/db-sqlite/files/drizzle.config.ts +10 -0
  76. package/packs/db-sqlite/files/lib/db.ts +8 -0
  77. package/packs/db-sqlite/files/lib/schema.ts +13 -0
  78. package/packs/db-sqlite/pack.toml +34 -0
  79. package/packs/db-sqlite/tasks.yaml +6 -0
  80. package/packs/db-supabase/files/lib/supabase/client.ts +8 -0
  81. package/packs/db-supabase/files/lib/supabase/server.ts +27 -0
  82. package/packs/db-supabase/pack.toml +32 -0
  83. package/packs/db-supabase/skills/supabase-patterns/SKILL.md +82 -0
  84. package/packs/db-supabase/tasks.yaml +6 -0
  85. package/packs/deploy-vercel/files/docs/deploy.md +21 -0
  86. package/packs/deploy-vercel/files/vercel.json +4 -0
  87. package/packs/deploy-vercel/pack.toml +30 -0
  88. package/packs/deploy-vercel/tasks.yaml +14 -0
  89. package/packs/docker-compose-dev/files/.env.docker.example +2 -0
  90. package/packs/docker-compose-dev/files/compose/redis.yml +17 -0
  91. package/packs/docker-compose-dev/files/docker-compose.include.yml +1 -0
  92. package/packs/docker-compose-dev/files/docker-compose.yml +6 -0
  93. package/packs/docker-compose-dev/pack.toml +38 -0
  94. package/packs/docker-compose-dev/tasks.yaml +9 -0
  95. package/packs/email-resend/files/app/api/email/test/route.ts +38 -0
  96. package/packs/email-resend/files/emails/welcome.tsx +66 -0
  97. package/packs/email-resend/files/lib/email.ts +40 -0
  98. package/packs/email-resend/pack.toml +34 -0
  99. package/packs/email-resend/tasks.yaml +9 -0
  100. package/packs/example/pack.toml +69 -0
  101. package/packs/payments-stripe/files/app/api/billing-portal/route.ts +24 -0
  102. package/packs/payments-stripe/files/app/api/checkout/route.ts +58 -0
  103. package/packs/payments-stripe/files/app/api/webhooks/stripe/route.ts +84 -0
  104. package/packs/payments-stripe/files/lib/stripe.ts +60 -0
  105. package/packs/payments-stripe/pack.toml +49 -0
  106. package/packs/payments-stripe/skills/stripe-patterns/SKILL.md +93 -0
  107. package/packs/payments-stripe/tasks.yaml +16 -0
  108. package/packs/sync-zero/files/components/ZeroProvider.tsx +3 -0
  109. package/packs/sync-zero/files/compose/zero-cache.yml +26 -0
  110. package/packs/sync-zero/files/docker-compose.include.yml +1 -0
  111. package/packs/sync-zero/files/docker-compose.yml +6 -0
  112. package/packs/sync-zero/files/lib/zero/client.ts +18 -0
  113. package/packs/sync-zero/files/lib/zero/schema.ts +17 -0
  114. package/packs/sync-zero/files/zero.config.ts +26 -0
  115. package/packs/sync-zero/pack.toml +61 -0
  116. package/packs/sync-zero/skills/zero-patterns/SKILL.md +69 -0
  117. package/packs/sync-zero/tasks.yaml +16 -0
  118. package/packs/testing-playwright/files/e2e/example.spec.ts +7 -0
  119. package/packs/testing-playwright/files/playwright.config.ts +33 -0
  120. package/packs/testing-playwright/pack.toml +25 -0
  121. package/packs/testing-playwright/tasks.yaml +9 -0
  122. package/packs/ui-shadcn/files/app/globals.css +56 -0
  123. package/packs/ui-shadcn/files/components/ui/button.tsx +47 -0
  124. package/packs/ui-shadcn/files/components/ui/card.tsx +33 -0
  125. package/packs/ui-shadcn/files/lib/utils.ts +6 -0
  126. package/packs/ui-shadcn/files/postcss.config.mjs +7 -0
  127. package/packs/ui-shadcn/files/tailwind.config.ts +57 -0
  128. package/packs/ui-shadcn/pack.toml +44 -0
  129. package/packs/ui-shadcn/skills/shadcn-dashboard-patterns/SKILL.md +85 -0
  130. package/packs/ui-shadcn/tasks.yaml +6 -0
  131. package/presets/docs-site.toml +4 -0
  132. package/presets/internal-tool.toml +4 -0
  133. package/presets/lean-saas.toml +4 -0
  134. package/presets/local-ai-mvp.toml +4 -0
  135. package/presets/saas-classic.toml +4 -0
  136. package/scripts/sync-skills.ts +223 -0
  137. package/src/paths.ts +22 -4
  138. package/templates/README.md +43 -0
  139. package/templates/astro/README.md +3 -0
  140. package/templates/astro/template.toml +4 -0
  141. package/templates/astro-starlight/README.md +3 -0
  142. package/templates/astro-starlight/template.toml +4 -0
  143. package/templates/nextjs/.ai/architecture.md +13 -0
  144. package/templates/nextjs/.ai/board.md +7 -0
  145. package/templates/nextjs/.ai/product-spec.md +11 -0
  146. package/templates/nextjs/.claude/skills/.gitkeep +0 -0
  147. package/templates/nextjs/.codex/skills/.gitkeep +0 -0
  148. package/templates/nextjs/AGENTS.md +95 -0
  149. package/templates/nextjs/CLAUDE.md +3 -0
  150. package/templates/nextjs/README.md +20 -0
  151. package/templates/nextjs/app/(app)/home/page.tsx +43 -0
  152. package/templates/nextjs/app/(app)/home/posts-panel.tsx +83 -0
  153. package/templates/nextjs/app/(app)/layout.tsx +12 -0
  154. package/templates/nextjs/app/(auth)/login/page.tsx +97 -0
  155. package/templates/nextjs/app/globals.css +23 -0
  156. package/templates/nextjs/app/layout.tsx +20 -0
  157. package/templates/nextjs/app/page.tsx +39 -0
  158. package/templates/nextjs/lib/auth-placeholder.ts +21 -0
  159. package/templates/nextjs/lib/posts-placeholder.ts +30 -0
  160. package/templates/nextjs/next.config.ts +5 -0
  161. package/templates/nextjs/package.json +26 -0
  162. package/templates/nextjs/postcss.config.mjs +7 -0
  163. package/templates/nextjs/spark.config.json +4 -0
  164. package/templates/nextjs/template.toml +4 -0
  165. package/templates/nextjs/tsconfig.json +27 -0
  166. package/templates/nextjs/types/post.ts +13 -0
  167. package/templates/one/README.md +5 -0
  168. package/templates/one/template.toml +4 -0
  169. package/templates/vite-react/README.md +3 -0
  170. package/templates/vite-react/template.toml +4 -0
@@ -0,0 +1,9 @@
1
+ epic: AI
2
+
3
+ tasks:
4
+ - id: AI-001
5
+ title: Add cost guardrails (token caps, rate limits)
6
+ status: Clarifying
7
+ acceptance:
8
+ - AI requests have server-side max token caps.
9
+ - Repeated requests are rate limited before calling Anthropic.
@@ -0,0 +1,55 @@
1
+ import { getOpenAIClient, OPENAI_CHAT_MODEL, type OpenAIChatMessage } from "../../../lib/openai";
2
+
3
+ export const runtime = "nodejs";
4
+
5
+ type ChatRequest = {
6
+ messages?: OpenAIChatMessage[];
7
+ prompt?: string;
8
+ };
9
+
10
+ function normalizeMessages(body: ChatRequest): OpenAIChatMessage[] {
11
+ if (Array.isArray(body.messages) && body.messages.length > 0) {
12
+ return body.messages;
13
+ }
14
+
15
+ if (body.prompt) {
16
+ return [{ role: "user", content: body.prompt }];
17
+ }
18
+
19
+ throw new Error("Request body must include messages or prompt.");
20
+ }
21
+
22
+ export async function POST(request: Request): Promise<Response> {
23
+ const body = (await request.json()) as ChatRequest;
24
+ const messages = normalizeMessages(body);
25
+
26
+ const stream = await getOpenAIClient().chat.completions.create({
27
+ model: OPENAI_CHAT_MODEL,
28
+ messages,
29
+ stream: true,
30
+ });
31
+
32
+ const encoder = new TextEncoder();
33
+ const readable = new ReadableStream<Uint8Array>({
34
+ async start(controller) {
35
+ try {
36
+ for await (const chunk of stream) {
37
+ const content = chunk.choices[0]?.delta?.content;
38
+ if (content) {
39
+ controller.enqueue(encoder.encode(content));
40
+ }
41
+ }
42
+ controller.close();
43
+ } catch (error) {
44
+ controller.error(error);
45
+ }
46
+ },
47
+ });
48
+
49
+ return new Response(readable, {
50
+ headers: {
51
+ "Cache-Control": "no-cache",
52
+ "Content-Type": "text/plain; charset=utf-8",
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,21 @@
1
+ import OpenAI from "openai";
2
+
3
+ let client: OpenAI | undefined;
4
+
5
+ export type OpenAIChatMessage = {
6
+ role: "developer" | "system" | "user" | "assistant";
7
+ content: string;
8
+ };
9
+
10
+ export const OPENAI_CHAT_MODEL = "gpt-5.2";
11
+
12
+ export function getOpenAIClient(): OpenAI {
13
+ const apiKey = process.env.OPENAI_API_KEY;
14
+
15
+ if (!apiKey) {
16
+ throw new Error("OPENAI_API_KEY is required to use the OpenAI client.");
17
+ }
18
+
19
+ client ??= new OpenAI({ apiKey });
20
+ return client;
21
+ }
@@ -0,0 +1,30 @@
1
+ name = "ai-openai"
2
+ version = "1.0.0"
3
+ category = "ai"
4
+ description = "OpenAI SDK client wrapper and streaming chat endpoint."
5
+ provides = ["ai-sdk"]
6
+ requires = []
7
+ conflicts = []
8
+ requires_runtime = ["server"]
9
+ compatible_scaffolds = []
10
+
11
+ [dependencies]
12
+ runtime = ["openai"]
13
+ dev = []
14
+
15
+ [env]
16
+ required = ["OPENAI_API_KEY"]
17
+ optional = []
18
+
19
+ [[files]]
20
+ mode = "create"
21
+ from = "lib/openai.ts"
22
+ to = "lib/openai.ts"
23
+
24
+ [[files]]
25
+ mode = "create"
26
+ from = "app/api/ai-openai/route.ts"
27
+ to = "app/api/ai-openai/route.ts"
28
+
29
+ [tasks]
30
+ file = "tasks.yaml"
@@ -0,0 +1,9 @@
1
+ epic: AI
2
+ tasks:
3
+ - id: AI-101
4
+ title: Add usage tracking + cost guardrails
5
+ status: Clarifying
6
+ acceptance:
7
+ - OpenAI requests record model, token usage, and user or workspace context.
8
+ - The endpoint enforces a documented per-user or per-workspace usage limit.
9
+ - Over-limit requests return a clear non-200 response without calling OpenAI.
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useEffect } from "react";
5
+ import posthog from "posthog-js";
6
+ import { PostHogProvider as Provider } from "posthog-js/react";
7
+ import { getPostHogClient } from "../lib/posthog/client";
8
+
9
+ type PostHogProviderProps = {
10
+ children: ReactNode;
11
+ };
12
+
13
+ export function PostHogProvider({ children }: PostHogProviderProps) {
14
+ useEffect(() => {
15
+ getPostHogClient();
16
+ }, []);
17
+
18
+ return <Provider client={posthog}>{children}</Provider>;
19
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import posthog from "posthog-js";
4
+
5
+ const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
6
+
7
+ export function getPostHogClient(): typeof posthog {
8
+ const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
9
+ const host = process.env.NEXT_PUBLIC_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST;
10
+ const loaded = (posthog as typeof posthog & { __loaded?: boolean }).__loaded;
11
+
12
+ if (typeof window !== "undefined" && key && !loaded) {
13
+ posthog.init(key, {
14
+ api_host: host,
15
+ capture_pageview: false,
16
+ });
17
+ }
18
+
19
+ return posthog;
20
+ }
@@ -0,0 +1,24 @@
1
+ import { PostHog } from "posthog-node";
2
+
3
+ const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
4
+
5
+ let client: PostHog | undefined;
6
+
7
+ export function getPostHogServerClient(): PostHog | undefined {
8
+ const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
9
+
10
+ if (!key) {
11
+ return undefined;
12
+ }
13
+
14
+ client ??= new PostHog(key, {
15
+ host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST,
16
+ });
17
+
18
+ return client;
19
+ }
20
+
21
+ export async function shutdownPostHogServerClient(): Promise<void> {
22
+ await client?.shutdown();
23
+ client = undefined;
24
+ }
@@ -0,0 +1,35 @@
1
+ name = "analytics-posthog"
2
+ version = "1.0.0"
3
+ category = "analytics"
4
+ description = "PostHog browser and server analytics helpers."
5
+ provides = ["analytics"]
6
+ requires = []
7
+ conflicts = []
8
+ requires_runtime = ["server"]
9
+ compatible_scaffolds = []
10
+
11
+ [dependencies]
12
+ runtime = ["posthog-js", "posthog-node"]
13
+ dev = []
14
+
15
+ [env]
16
+ required = ["NEXT_PUBLIC_POSTHOG_KEY"]
17
+ optional = ["NEXT_PUBLIC_POSTHOG_HOST"]
18
+
19
+ [[files]]
20
+ mode = "create"
21
+ from = "lib/posthog/client.ts"
22
+ to = "lib/posthog/client.ts"
23
+
24
+ [[files]]
25
+ mode = "create"
26
+ from = "lib/posthog/server.ts"
27
+ to = "lib/posthog/server.ts"
28
+
29
+ [[files]]
30
+ mode = "create"
31
+ from = "components/PostHogProvider.tsx"
32
+ to = "components/PostHogProvider.tsx"
33
+
34
+ [tasks]
35
+ file = "tasks.yaml"
@@ -0,0 +1,15 @@
1
+ epic: Analytics
2
+ tasks:
3
+ - id: ANALYTICS-001
4
+ title: Wire posthog provider into app root
5
+ status: Clarifying
6
+ acceptance:
7
+ - The app root wraps client routes with PostHogProvider.
8
+ - Page views are captured intentionally, either by router events or a documented manual call.
9
+ - id: ANALYTICS-002
10
+ title: Add consent banner
11
+ status: Clarifying
12
+ acceptance:
13
+ - Users can accept or decline analytics before tracking starts.
14
+ - The consent choice persists across reloads.
15
+ - PostHog capture is disabled when consent is declined.
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useState, useTransition } from 'react';
4
+ import { createAuthClient } from 'better-auth/react';
5
+
6
+ const authClient = createAuthClient();
7
+ type SocialProvider = 'github' | 'google';
8
+
9
+ export default function LoginPage() {
10
+ const [error, setError] = useState<string | null>(null);
11
+ const [isPending, startTransition] = useTransition();
12
+
13
+ function signIn(provider: SocialProvider) {
14
+ setError(null);
15
+ startTransition(() => {
16
+ void authClient.signIn
17
+ .social({
18
+ provider,
19
+ callbackURL: '/',
20
+ })
21
+ .then((result) => {
22
+ if (result.error) {
23
+ setError(result.error.message ?? 'Unable to sign in.');
24
+ }
25
+ });
26
+ });
27
+ }
28
+
29
+ return (
30
+ <main className="mx-auto flex min-h-screen w-full max-w-sm flex-col justify-center px-6">
31
+ <div className="space-y-2">
32
+ <h1 className="text-2xl font-semibold tracking-tight">Sign in</h1>
33
+ <p className="text-sm text-muted-foreground">Continue with a configured OAuth provider.</p>
34
+ </div>
35
+
36
+ <div className="mt-8 grid gap-3">
37
+ <button
38
+ className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
39
+ disabled={isPending}
40
+ onClick={() => signIn('github')}
41
+ type="button"
42
+ >
43
+ Continue with GitHub
44
+ </button>
45
+ <button
46
+ className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
47
+ disabled={isPending}
48
+ onClick={() => signIn('google')}
49
+ type="button"
50
+ >
51
+ Continue with Google
52
+ </button>
53
+ </div>
54
+
55
+ {error ? <p className="mt-4 text-sm text-destructive">{error}</p> : null}
56
+ </main>
57
+ );
58
+ }
@@ -0,0 +1,4 @@
1
+ import { createAuthHandler } from '@forgeailab/spark-auth-better-auth';
2
+ import { auth } from '@/lib/auth';
3
+
4
+ export const { GET, POST } = createAuthHandler(auth);
@@ -0,0 +1,21 @@
1
+ import { createAuth as createBetterAuth } from '@forgeailab/spark-auth-better-auth';
2
+
3
+ // Wire your database adapter here. Example (drizzle + sqlite):
4
+ //
5
+ // import { drizzleAdapter } from '@better-auth/drizzle-adapter';
6
+ // import { db } from '@/lib/db';
7
+ // import * as schema from '@/lib/db/schema';
8
+ // const adapter = drizzleAdapter(db, { provider: 'sqlite', schema });
9
+ //
10
+ // Then pass the adapter into createAuth({ adapter, ... }).
11
+
12
+ export const auth = createBetterAuth({
13
+ adapter: undefined as never, // TODO: replace with your drizzle adapter
14
+ secret: process.env.BETTER_AUTH_SECRET!,
15
+ baseURL: process.env.BETTER_AUTH_URL,
16
+ emailAndPassword: {
17
+ enabled: true,
18
+ },
19
+ });
20
+
21
+ export type Auth = typeof auth;
@@ -0,0 +1,32 @@
1
+ name = "auth-better-auth"
2
+ version = "1.0.0"
3
+ category = "auth"
4
+ description = "Better Auth route handler, auth instance, and login page for Next.js."
5
+ provides = ["auth"]
6
+ requires = ["db"]
7
+ conflicts = ["auth"]
8
+ requires_runtime = ["server"]
9
+ compatible_scaffolds = ["nextjs"]
10
+
11
+ [runtime_package]
12
+ package = "@forgeailab/spark-auth-better-auth"
13
+ version = "^0.1"
14
+
15
+ [dependencies]
16
+ runtime = ["@better-auth/drizzle-adapter"]
17
+
18
+ [env]
19
+ required = ["BETTER_AUTH_SECRET", "BETTER_AUTH_URL"]
20
+
21
+ [[files]]
22
+ mode = "create"
23
+ from = "lib/auth.ts"
24
+ to = "lib/auth.ts"
25
+
26
+ [[files]]
27
+ mode = "create"
28
+ from = "app/api/auth/[...all]/route.ts"
29
+ to = "app/api/auth/[...all]/route.ts"
30
+
31
+ [tasks]
32
+ file = "tasks.yaml"
@@ -0,0 +1,10 @@
1
+ epic: Auth
2
+ tasks:
3
+ - id: AUTH-101
4
+ title: Wire auth instance to active db adapter
5
+ acceptance:
6
+ - Better Auth persists users, sessions, and accounts through the active db adapter
7
+ - id: AUTH-102
8
+ title: Configure OAuth providers (Google/GitHub)
9
+ acceptance:
10
+ - Google and GitHub OAuth providers can complete a sign-in flow
@@ -0,0 +1,4 @@
1
+ import { createAuthHandler } from '@forgeailab/spark-auth-better-auth';
2
+ import { auth } from '@/lib/auth';
3
+
4
+ export const { GET, POST } = createAuthHandler(auth);
@@ -0,0 +1,86 @@
1
+ import { drizzleAdapter } from '@better-auth/drizzle-adapter';
2
+ import { createAuth as createBetterAuth } from '@forgeailab/spark-auth-better-auth';
3
+ import { db } from '@/lib/db';
4
+ import * as schema from '@/lib/db/schema';
5
+
6
+ // Postgres-flavored Better Auth wiring. Pair this pack with `db-postgres`
7
+ // (or `db-supabase`), and add the four Better Auth tables to your
8
+ // `lib/db/schema.ts`: `user`, `session`, `account`, `verification`. Snake-case
9
+ // column names are what Better Auth expects.
10
+
11
+ const DEV_SECRET =
12
+ 'reference-dev-secret-change-me-reference-dev-secret-change-me';
13
+
14
+ function env(name: string, fallback: string): string {
15
+ return process.env[name] ?? fallback;
16
+ }
17
+
18
+ function resolveTrustedOrigins(baseURL: string): string[] {
19
+ const fromEnv = process.env.BETTER_AUTH_TRUSTED_ORIGINS;
20
+ if (fromEnv) {
21
+ return fromEnv
22
+ .split(',')
23
+ .map((s) => s.trim())
24
+ .filter(Boolean);
25
+ }
26
+
27
+ const origins = new Set<string>([baseURL]);
28
+
29
+ // In dev, Next.js auto-bumps the port when 3000 is busy. Trust common
30
+ // localhost ports so signup doesn't break with "Invalid origin" just because
31
+ // another server already owns 3000. Override via BETTER_AUTH_TRUSTED_ORIGINS
32
+ // for production lockdown.
33
+ if (process.env.NODE_ENV !== 'production') {
34
+ for (const port of [3000, 3001, 3002, 3003, 3010, 4000, 5173, 8080]) {
35
+ origins.add(`http://localhost:${port}`);
36
+ origins.add(`http://127.0.0.1:${port}`);
37
+ }
38
+ }
39
+
40
+ return [...origins];
41
+ }
42
+
43
+ export function createAuthDatabase() {
44
+ return drizzleAdapter(db, {
45
+ provider: 'pg',
46
+ schema: {
47
+ ...schema,
48
+ // Map your Drizzle table exports to the names Better Auth uses. Uncomment
49
+ // these once your schema has the matching tables (or use these names
50
+ // directly in your schema and drop the aliases).
51
+ // user: schema.users,
52
+ // session: schema.sessions,
53
+ // account: schema.accounts,
54
+ // verification: schema.verifications,
55
+ },
56
+ });
57
+ }
58
+
59
+ type CreateAuthOptions = {
60
+ database?: ReturnType<typeof createAuthDatabase>;
61
+ };
62
+
63
+ export function createAuth(options: CreateAuthOptions = {}) {
64
+ const baseURL = env('BETTER_AUTH_URL', 'http://localhost:3000');
65
+
66
+ return createBetterAuth({
67
+ adapter: options.database ?? createAuthDatabase(),
68
+ baseURL,
69
+ secret: env('BETTER_AUTH_SECRET', DEV_SECRET),
70
+ trustedOrigins: resolveTrustedOrigins(baseURL),
71
+ emailAndPassword: {
72
+ enabled: true,
73
+ },
74
+ // Uncomment to enable GitHub OAuth. Add env vars to your .env.local and
75
+ // register the OAuth app at https://github.com/settings/developers.
76
+ // socialProviders: {
77
+ // github: {
78
+ // clientId: env('GITHUB_CLIENT_ID', 'github-client-id'),
79
+ // clientSecret: env('GITHUB_CLIENT_SECRET', 'github-client-secret'),
80
+ // },
81
+ // },
82
+ });
83
+ }
84
+
85
+ export const auth = createAuth();
86
+ export type Auth = typeof auth;
@@ -0,0 +1,32 @@
1
+ name = "auth-better-auth-pg"
2
+ version = "1.0.0"
3
+ category = "auth"
4
+ description = "Better Auth wired to Postgres via Drizzle. Pairs with db-postgres or db-supabase."
5
+ provides = ["auth"]
6
+ requires = ["db-pg"]
7
+ conflicts = ["auth"]
8
+ requires_runtime = ["server"]
9
+ compatible_scaffolds = ["nextjs"]
10
+
11
+ [runtime_package]
12
+ package = "@forgeailab/spark-auth-better-auth"
13
+ version = "^0.1"
14
+
15
+ [dependencies]
16
+ runtime = ["@better-auth/drizzle-adapter"]
17
+
18
+ [env]
19
+ required = ["BETTER_AUTH_SECRET", "BETTER_AUTH_URL"]
20
+
21
+ [[files]]
22
+ mode = "create"
23
+ from = "lib/auth.ts"
24
+ to = "lib/auth.ts"
25
+
26
+ [[files]]
27
+ mode = "create"
28
+ from = "app/api/auth/[...all]/route.ts"
29
+ to = "app/api/auth/[...all]/route.ts"
30
+
31
+ [tasks]
32
+ file = "tasks.yaml"
@@ -0,0 +1,17 @@
1
+ epic: Auth
2
+ tasks:
3
+ - id: AUTH-201
4
+ title: Add Better Auth tables to your Drizzle schema
5
+ acceptance:
6
+ - lib/db/schema.ts defines user, session, account, verification with pgTable
7
+ - Snake-case columns (email_verified, expires_at, user_id, etc.) match Better Auth contract
8
+ - `bun drizzle-kit push` applies the new tables to Postgres
9
+ - id: AUTH-202
10
+ title: Map schema exports in lib/auth.ts
11
+ acceptance:
12
+ - `createAuthDatabase()` passes drizzleAdapter the four required schema aliases
13
+ - `bunx tsc --noEmit` is clean
14
+ - id: AUTH-203
15
+ title: Configure OAuth providers (Google/GitHub) if needed
16
+ acceptance:
17
+ - `socialProviders` set in createAuth options with env-backed credentials
@@ -0,0 +1,64 @@
1
+ import type { Provider } from '@supabase/supabase-js';
2
+ import { headers } from 'next/headers';
3
+ import { redirect } from 'next/navigation';
4
+ import { createSupabaseServerClient } from '@/lib/supabase/server';
5
+
6
+ async function signInWithOAuth(formData: FormData) {
7
+ 'use server';
8
+
9
+ const rawProvider = formData.get('provider');
10
+ if (rawProvider !== 'github' && rawProvider !== 'google') {
11
+ redirect('/login?error=unsupported-provider');
12
+ }
13
+
14
+ const provider: Provider = rawProvider;
15
+ const requestHeaders = await headers();
16
+ const origin = requestHeaders.get('origin') ?? 'http://localhost:3000';
17
+ const supabase = await createSupabaseServerClient();
18
+ const { data, error } = await supabase.auth.signInWithOAuth({
19
+ provider,
20
+ options: {
21
+ redirectTo: `${origin}/auth/callback`,
22
+ },
23
+ });
24
+
25
+ if (error) {
26
+ redirect(`/login?error=${encodeURIComponent(error.message)}`);
27
+ }
28
+
29
+ if (data.url) {
30
+ redirect(data.url);
31
+ }
32
+
33
+ redirect('/login?error=missing-redirect-url');
34
+ }
35
+
36
+ export default function LoginPage() {
37
+ return (
38
+ <main className="mx-auto flex min-h-screen w-full max-w-sm flex-col justify-center px-6">
39
+ <div className="space-y-2">
40
+ <h1 className="text-2xl font-semibold tracking-tight">Sign in</h1>
41
+ <p className="text-sm text-muted-foreground">Choose an OAuth provider to continue.</p>
42
+ </div>
43
+
44
+ <form action={signInWithOAuth} className="mt-8 grid gap-3">
45
+ <button
46
+ className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
47
+ name="provider"
48
+ type="submit"
49
+ value="github"
50
+ >
51
+ Continue with GitHub
52
+ </button>
53
+ <button
54
+ className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
55
+ name="provider"
56
+ type="submit"
57
+ value="google"
58
+ >
59
+ Continue with Google
60
+ </button>
61
+ </form>
62
+ </main>
63
+ );
64
+ }
@@ -0,0 +1,15 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { createSupabaseServerClient } from '@/lib/supabase/server';
3
+
4
+ export async function GET(request: NextRequest) {
5
+ const requestUrl = new URL(request.url);
6
+ const code = requestUrl.searchParams.get('code');
7
+ const next = requestUrl.searchParams.get('next') ?? '/';
8
+
9
+ if (code) {
10
+ const supabase = await createSupabaseServerClient();
11
+ await supabase.auth.exchangeCodeForSession(code);
12
+ }
13
+
14
+ return NextResponse.redirect(new URL(next, requestUrl.origin));
15
+ }
@@ -0,0 +1,41 @@
1
+ import { createServerClient } from '@supabase/ssr';
2
+ import { NextResponse, type NextRequest } from 'next/server';
3
+
4
+ export async function middleware(request: NextRequest) {
5
+ let response = NextResponse.next({
6
+ request,
7
+ });
8
+
9
+ const supabase = createServerClient(
10
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
11
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12
+ {
13
+ cookies: {
14
+ getAll() {
15
+ return request.cookies.getAll();
16
+ },
17
+ setAll(cookiesToSet) {
18
+ cookiesToSet.forEach(({ name, value }) => {
19
+ request.cookies.set(name, value);
20
+ });
21
+
22
+ response = NextResponse.next({
23
+ request,
24
+ });
25
+
26
+ cookiesToSet.forEach(({ name, value, options }) => {
27
+ response.cookies.set(name, value, options);
28
+ });
29
+ },
30
+ },
31
+ },
32
+ );
33
+
34
+ await supabase.auth.getUser();
35
+
36
+ return response;
37
+ }
38
+
39
+ export const config = {
40
+ matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
41
+ };