@forgeailab/create-spark 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -4
- package/packs/README.md +132 -0
- package/packs/ai-anthropic/files/app/api/ai/route.ts +57 -0
- package/packs/ai-anthropic/files/lib/anthropic.ts +15 -0
- package/packs/ai-anthropic/pack.toml +32 -0
- package/packs/ai-anthropic/skills/ai-feature-patterns/SKILL.md +87 -0
- package/packs/ai-anthropic/tasks.yaml +9 -0
- package/packs/ai-openai/files/app/api/ai-openai/route.ts +55 -0
- package/packs/ai-openai/files/lib/openai.ts +21 -0
- package/packs/ai-openai/pack.toml +30 -0
- package/packs/ai-openai/tasks.yaml +9 -0
- package/packs/analytics-posthog/files/components/PostHogProvider.tsx +19 -0
- package/packs/analytics-posthog/files/lib/posthog/client.ts +20 -0
- package/packs/analytics-posthog/files/lib/posthog/server.ts +24 -0
- package/packs/analytics-posthog/pack.toml +35 -0
- package/packs/analytics-posthog/tasks.yaml +15 -0
- package/packs/auth-better-auth/files/app/(auth)/login/page.tsx +58 -0
- package/packs/auth-better-auth/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth/files/lib/auth.ts +21 -0
- package/packs/auth-better-auth/pack.toml +32 -0
- package/packs/auth-better-auth/tasks.yaml +10 -0
- package/packs/auth-better-auth-pg/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth-pg/files/lib/auth.ts +86 -0
- package/packs/auth-better-auth-pg/pack.toml +32 -0
- package/packs/auth-better-auth-pg/tasks.yaml +17 -0
- package/packs/auth-supabase/files/app/(auth)/login/page.tsx +64 -0
- package/packs/auth-supabase/files/app/auth/callback/route.ts +15 -0
- package/packs/auth-supabase/files/middleware.ts +41 -0
- package/packs/auth-supabase/pack.toml +34 -0
- package/packs/auth-supabase/tasks.yaml +10 -0
- package/packs/db-postgres/files/compose/postgres.yml +28 -0
- package/packs/db-postgres/files/docker-compose.include.yml +1 -0
- package/packs/db-postgres/files/docker-compose.yml +6 -0
- package/packs/db-postgres/files/drizzle.config.ts +10 -0
- package/packs/db-postgres/files/lib/db/index.ts +10 -0
- package/packs/db-postgres/files/lib/db/schema.ts +11 -0
- package/packs/db-postgres/pack.toml +53 -0
- package/packs/db-postgres/tasks.yaml +11 -0
- package/packs/db-sqlite/files/drizzle.config.ts +10 -0
- package/packs/db-sqlite/files/lib/db.ts +8 -0
- package/packs/db-sqlite/files/lib/schema.ts +13 -0
- package/packs/db-sqlite/pack.toml +34 -0
- package/packs/db-sqlite/tasks.yaml +6 -0
- package/packs/db-supabase/files/lib/supabase/client.ts +8 -0
- package/packs/db-supabase/files/lib/supabase/server.ts +27 -0
- package/packs/db-supabase/pack.toml +32 -0
- package/packs/db-supabase/skills/supabase-patterns/SKILL.md +82 -0
- package/packs/db-supabase/tasks.yaml +6 -0
- package/packs/deploy-vercel/files/docs/deploy.md +21 -0
- package/packs/deploy-vercel/files/vercel.json +4 -0
- package/packs/deploy-vercel/pack.toml +30 -0
- package/packs/deploy-vercel/tasks.yaml +14 -0
- package/packs/docker-compose-dev/files/.env.docker.example +2 -0
- package/packs/docker-compose-dev/files/compose/redis.yml +17 -0
- package/packs/docker-compose-dev/files/docker-compose.include.yml +1 -0
- package/packs/docker-compose-dev/files/docker-compose.yml +6 -0
- package/packs/docker-compose-dev/pack.toml +38 -0
- package/packs/docker-compose-dev/tasks.yaml +9 -0
- package/packs/email-resend/files/app/api/email/test/route.ts +38 -0
- package/packs/email-resend/files/emails/welcome.tsx +66 -0
- package/packs/email-resend/files/lib/email.ts +40 -0
- package/packs/email-resend/pack.toml +34 -0
- package/packs/email-resend/tasks.yaml +9 -0
- package/packs/example/pack.toml +69 -0
- package/packs/payments-stripe/files/app/api/billing-portal/route.ts +24 -0
- package/packs/payments-stripe/files/app/api/checkout/route.ts +58 -0
- package/packs/payments-stripe/files/app/api/webhooks/stripe/route.ts +84 -0
- package/packs/payments-stripe/files/lib/stripe.ts +60 -0
- package/packs/payments-stripe/pack.toml +49 -0
- package/packs/payments-stripe/skills/stripe-patterns/SKILL.md +93 -0
- package/packs/payments-stripe/tasks.yaml +16 -0
- package/packs/sync-zero/files/components/ZeroProvider.tsx +3 -0
- package/packs/sync-zero/files/compose/zero-cache.yml +26 -0
- package/packs/sync-zero/files/docker-compose.include.yml +1 -0
- package/packs/sync-zero/files/docker-compose.yml +6 -0
- package/packs/sync-zero/files/lib/zero/client.ts +18 -0
- package/packs/sync-zero/files/lib/zero/schema.ts +17 -0
- package/packs/sync-zero/files/zero.config.ts +26 -0
- package/packs/sync-zero/pack.toml +61 -0
- package/packs/sync-zero/skills/zero-patterns/SKILL.md +69 -0
- package/packs/sync-zero/tasks.yaml +16 -0
- package/packs/testing-playwright/files/e2e/example.spec.ts +7 -0
- package/packs/testing-playwright/files/playwright.config.ts +33 -0
- package/packs/testing-playwright/pack.toml +25 -0
- package/packs/testing-playwright/tasks.yaml +9 -0
- package/packs/ui-shadcn/files/app/globals.css +56 -0
- package/packs/ui-shadcn/files/components/ui/button.tsx +47 -0
- package/packs/ui-shadcn/files/components/ui/card.tsx +33 -0
- package/packs/ui-shadcn/files/lib/utils.ts +6 -0
- package/packs/ui-shadcn/files/postcss.config.mjs +7 -0
- package/packs/ui-shadcn/files/tailwind.config.ts +57 -0
- package/packs/ui-shadcn/pack.toml +44 -0
- package/packs/ui-shadcn/skills/shadcn-dashboard-patterns/SKILL.md +85 -0
- package/packs/ui-shadcn/tasks.yaml +6 -0
- package/presets/docs-site.toml +4 -0
- package/presets/internal-tool.toml +4 -0
- package/presets/lean-saas.toml +4 -0
- package/presets/local-ai-mvp.toml +4 -0
- package/presets/saas-classic.toml +4 -0
- package/src/paths.ts +22 -4
- package/templates/README.md +43 -0
- package/templates/astro/README.md +3 -0
- package/templates/astro/template.toml +4 -0
- package/templates/astro-starlight/README.md +3 -0
- package/templates/astro-starlight/template.toml +4 -0
- package/templates/nextjs/.ai/architecture.md +13 -0
- package/templates/nextjs/.ai/board.md +7 -0
- package/templates/nextjs/.ai/product-spec.md +11 -0
- package/templates/nextjs/.claude/skills/.gitkeep +0 -0
- package/templates/nextjs/.codex/skills/.gitkeep +0 -0
- package/templates/nextjs/AGENTS.md +95 -0
- package/templates/nextjs/CLAUDE.md +3 -0
- package/templates/nextjs/README.md +20 -0
- package/templates/nextjs/anvil.config.json +4 -0
- package/templates/nextjs/app/(app)/home/page.tsx +43 -0
- package/templates/nextjs/app/(app)/home/posts-panel.tsx +83 -0
- package/templates/nextjs/app/(app)/layout.tsx +12 -0
- package/templates/nextjs/app/(auth)/login/page.tsx +97 -0
- package/templates/nextjs/app/globals.css +23 -0
- package/templates/nextjs/app/layout.tsx +20 -0
- package/templates/nextjs/app/page.tsx +39 -0
- package/templates/nextjs/lib/auth-placeholder.ts +21 -0
- package/templates/nextjs/lib/posts-placeholder.ts +30 -0
- package/templates/nextjs/next.config.ts +5 -0
- package/templates/nextjs/package.json +26 -0
- package/templates/nextjs/postcss.config.mjs +7 -0
- package/templates/nextjs/template.toml +4 -0
- package/templates/nextjs/tsconfig.json +27 -0
- package/templates/nextjs/types/post.ts +13 -0
- package/templates/one/README.md +5 -0
- package/templates/one/template.toml +4 -0
- package/templates/vite-react/README.md +3 -0
- package/templates/vite-react/template.toml +4 -0
|
@@ -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,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
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name = "auth-supabase"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "auth"
|
|
4
|
+
description = "Supabase Auth routes, login page, and session middleware for Next.js."
|
|
5
|
+
provides = ["auth"]
|
|
6
|
+
requires = ["db-pg"]
|
|
7
|
+
conflicts = ["auth"]
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
# NOTE: This pack is meaningfully compatible only when db-supabase is the installed db provider.
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
runtime = ["@supabase/supabase-js", "@supabase/ssr"]
|
|
15
|
+
|
|
16
|
+
[env]
|
|
17
|
+
required = ["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY"]
|
|
18
|
+
|
|
19
|
+
[[files]]
|
|
20
|
+
mode = "create"
|
|
21
|
+
from = "app/auth/callback/route.ts"
|
|
22
|
+
to = "app/auth/callback/route.ts"
|
|
23
|
+
|
|
24
|
+
# The nextjs template ships app/(auth)/login/page.tsx as a placeholder;
|
|
25
|
+
# Supabase users wire the supabase client into that template-owned file rather
|
|
26
|
+
# than overwriting it from the pack.
|
|
27
|
+
|
|
28
|
+
[[files]]
|
|
29
|
+
mode = "create"
|
|
30
|
+
from = "middleware.ts"
|
|
31
|
+
to = "middleware.ts"
|
|
32
|
+
|
|
33
|
+
[tasks]
|
|
34
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
epic: Auth
|
|
2
|
+
tasks:
|
|
3
|
+
- id: AUTH-001
|
|
4
|
+
title: Configure OAuth provider in Supabase dashboard
|
|
5
|
+
acceptance:
|
|
6
|
+
- Supabase OAuth provider completes sign-in through /auth/callback
|
|
7
|
+
- id: AUTH-002
|
|
8
|
+
title: Add protected route example
|
|
9
|
+
acceptance:
|
|
10
|
+
- Anonymous users are redirected away from the protected route example
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16
|
|
4
|
+
restart: unless-stopped
|
|
5
|
+
command:
|
|
6
|
+
- postgres
|
|
7
|
+
- -c
|
|
8
|
+
- wal_level=logical
|
|
9
|
+
- -c
|
|
10
|
+
- max_wal_senders=10
|
|
11
|
+
- -c
|
|
12
|
+
- max_replication_slots=10
|
|
13
|
+
environment:
|
|
14
|
+
POSTGRES_DB: ${POSTGRES_DB:-app}
|
|
15
|
+
POSTGRES_USER: ${POSTGRES_USER:-app}
|
|
16
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app}
|
|
17
|
+
ports:
|
|
18
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
19
|
+
volumes:
|
|
20
|
+
- postgres_data:/var/lib/postgresql/data
|
|
21
|
+
healthcheck:
|
|
22
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-app}"]
|
|
23
|
+
interval: 10s
|
|
24
|
+
timeout: 5s
|
|
25
|
+
retries: 5
|
|
26
|
+
|
|
27
|
+
volumes:
|
|
28
|
+
postgres_data:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- compose/postgres.yml
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from 'drizzle-kit';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
schema: './lib/db/schema.ts',
|
|
5
|
+
out: './drizzle',
|
|
6
|
+
dialect: 'postgresql',
|
|
7
|
+
dbCredentials: {
|
|
8
|
+
url: process.env.DATABASE_URL ?? 'postgres://postgres:postgres@localhost:5432/postgres',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import postgres from 'postgres';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
3
|
+
import * as schema from './schema';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_URL = 'postgres://postgres:postgres@localhost:5432/postgres';
|
|
6
|
+
const url = process.env.DATABASE_URL ?? DEFAULT_URL;
|
|
7
|
+
|
|
8
|
+
export const sql = postgres(url, { max: 10 });
|
|
9
|
+
export const db = drizzle(sql, { schema });
|
|
10
|
+
export type Db = typeof db;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
2
|
+
|
|
3
|
+
export const users = pgTable('users', {
|
|
4
|
+
id: text('id').primaryKey(),
|
|
5
|
+
email: text('email').notNull().unique(),
|
|
6
|
+
name: text('name'),
|
|
7
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type User = typeof users.$inferSelect;
|
|
11
|
+
export type NewUser = typeof users.$inferInsert;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name = "db-postgres"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "db"
|
|
4
|
+
description = "Postgres database setup with Drizzle ORM and postgres-js (Bun-compatible)."
|
|
5
|
+
provides = ["db", "db-pg"]
|
|
6
|
+
requires = []
|
|
7
|
+
conflicts = ["db"]
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
runtime = ["drizzle-orm", "postgres"]
|
|
13
|
+
dev = ["drizzle-kit"]
|
|
14
|
+
|
|
15
|
+
[env]
|
|
16
|
+
required = ["DATABASE_URL"]
|
|
17
|
+
|
|
18
|
+
[[files]]
|
|
19
|
+
mode = "create"
|
|
20
|
+
from = "lib/db/index.ts"
|
|
21
|
+
to = "lib/db/index.ts"
|
|
22
|
+
|
|
23
|
+
[[files]]
|
|
24
|
+
mode = "create"
|
|
25
|
+
from = "lib/db/schema.ts"
|
|
26
|
+
to = "lib/db/schema.ts"
|
|
27
|
+
|
|
28
|
+
[[files]]
|
|
29
|
+
mode = "create"
|
|
30
|
+
from = "drizzle.config.ts"
|
|
31
|
+
to = "drizzle.config.ts"
|
|
32
|
+
|
|
33
|
+
# Compose root + postgres fragment. Multiple packs (db-postgres, sync-zero,
|
|
34
|
+
# docker-compose-dev) each ship the same root file with `create-or-skip` —
|
|
35
|
+
# whichever installs first creates it, the rest no-op. Each pack then appends
|
|
36
|
+
# its own include line under a marker.
|
|
37
|
+
[[files]]
|
|
38
|
+
mode = "create-or-skip"
|
|
39
|
+
from = "docker-compose.yml"
|
|
40
|
+
to = "docker-compose.yml"
|
|
41
|
+
|
|
42
|
+
[[files]]
|
|
43
|
+
mode = "create"
|
|
44
|
+
from = "compose/postgres.yml"
|
|
45
|
+
to = "compose/postgres.yml"
|
|
46
|
+
|
|
47
|
+
[[files]]
|
|
48
|
+
mode = "append"
|
|
49
|
+
from = "docker-compose.include.yml"
|
|
50
|
+
to = "docker-compose.yml"
|
|
51
|
+
|
|
52
|
+
[tasks]
|
|
53
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
epic: Database
|
|
2
|
+
tasks:
|
|
3
|
+
- id: DB-001
|
|
4
|
+
title: Run initial migration
|
|
5
|
+
acceptance:
|
|
6
|
+
- Schema applied to Postgres via `bun drizzle-kit push`
|
|
7
|
+
- id: DB-002
|
|
8
|
+
title: Provision local Postgres
|
|
9
|
+
acceptance:
|
|
10
|
+
- Postgres reachable at DATABASE_URL (docker-compose, Homebrew, or hosted)
|
|
11
|
+
- wal_level=logical set if pairing with sync-zero
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
3
|
+
import * as schema from './schema';
|
|
4
|
+
|
|
5
|
+
const databaseUrl = process.env.DATABASE_URL ?? 'local.db';
|
|
6
|
+
const sqlite = new Database(databaseUrl.replace(/^file:/u, ''));
|
|
7
|
+
|
|
8
|
+
export const db = drizzle(sqlite, { schema });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
|
|
3
|
+
export const users = sqliteTable('users', {
|
|
4
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
5
|
+
email: text('email').notNull().unique(),
|
|
6
|
+
name: text('name'),
|
|
7
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
8
|
+
.notNull()
|
|
9
|
+
.$defaultFn(() => new Date()),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type User = typeof users.$inferSelect;
|
|
13
|
+
export type NewUser = typeof users.$inferInsert;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name = "db-sqlite"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "db"
|
|
4
|
+
description = "SQLite database setup with Drizzle ORM and Bun."
|
|
5
|
+
provides = ["db"]
|
|
6
|
+
requires = []
|
|
7
|
+
conflicts = ["db"]
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
runtime = ["drizzle-orm"]
|
|
13
|
+
dev = ["drizzle-kit"]
|
|
14
|
+
|
|
15
|
+
[env]
|
|
16
|
+
required = ["DATABASE_URL"]
|
|
17
|
+
|
|
18
|
+
[[files]]
|
|
19
|
+
mode = "create"
|
|
20
|
+
from = "lib/db.ts"
|
|
21
|
+
to = "lib/db.ts"
|
|
22
|
+
|
|
23
|
+
[[files]]
|
|
24
|
+
mode = "create"
|
|
25
|
+
from = "lib/schema.ts"
|
|
26
|
+
to = "lib/schema.ts"
|
|
27
|
+
|
|
28
|
+
[[files]]
|
|
29
|
+
mode = "create"
|
|
30
|
+
from = "drizzle.config.ts"
|
|
31
|
+
to = "drizzle.config.ts"
|
|
32
|
+
|
|
33
|
+
[tasks]
|
|
34
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createServerClient } from '@supabase/ssr';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
export async function createSupabaseServerClient() {
|
|
5
|
+
const cookieStore = await cookies();
|
|
6
|
+
|
|
7
|
+
return createServerClient(
|
|
8
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
9
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
10
|
+
{
|
|
11
|
+
cookies: {
|
|
12
|
+
getAll() {
|
|
13
|
+
return cookieStore.getAll();
|
|
14
|
+
},
|
|
15
|
+
setAll(cookiesToSet) {
|
|
16
|
+
try {
|
|
17
|
+
cookiesToSet.forEach(({ name, value, options }) => {
|
|
18
|
+
cookieStore.set(name, value, options);
|
|
19
|
+
});
|
|
20
|
+
} catch {
|
|
21
|
+
// Server Components cannot write cookies. Middleware refreshes sessions.
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name = "db-supabase"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "db"
|
|
4
|
+
description = "Supabase browser and server clients for Next.js."
|
|
5
|
+
provides = ["db", "db-pg"]
|
|
6
|
+
requires = []
|
|
7
|
+
conflicts = ["db"]
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
runtime = ["@supabase/supabase-js", "@supabase/ssr"]
|
|
13
|
+
|
|
14
|
+
[env]
|
|
15
|
+
required = ["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY"]
|
|
16
|
+
optional = ["SUPABASE_SERVICE_ROLE_KEY"]
|
|
17
|
+
|
|
18
|
+
[[files]]
|
|
19
|
+
mode = "create"
|
|
20
|
+
from = "lib/supabase/server.ts"
|
|
21
|
+
to = "lib/supabase/server.ts"
|
|
22
|
+
|
|
23
|
+
[[files]]
|
|
24
|
+
mode = "create"
|
|
25
|
+
from = "lib/supabase/client.ts"
|
|
26
|
+
to = "lib/supabase/client.ts"
|
|
27
|
+
|
|
28
|
+
[skills]
|
|
29
|
+
copy = ["skills/supabase-patterns"]
|
|
30
|
+
|
|
31
|
+
[tasks]
|
|
32
|
+
file = "tasks.yaml"
|