create-nextblock 0.8.11 → 0.9.5

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 (54) hide show
  1. package/bin/create-nextblock.js +101 -35
  2. package/docker-template/.dockerignore +23 -0
  3. package/docker-template/.env.docker.example +56 -0
  4. package/docker-template/Dockerfile +85 -0
  5. package/docker-template/docker/db/init/99-jwt.sql +6 -0
  6. package/docker-template/docker/db/init/99-roles.sql +25 -0
  7. package/docker-template/docker/kong/kong.yml +112 -0
  8. package/docker-template/docker/migrate/run-migrations.sh +51 -0
  9. package/docker-template/docker-compose.yml +219 -0
  10. package/docker-template/scripts/docker-setup.mjs +242 -0
  11. package/package.json +1 -1
  12. package/scripts/sync-template.js +29 -0
  13. package/templates/nextblock-template/.dockerignore +23 -0
  14. package/templates/nextblock-template/Dockerfile +85 -0
  15. package/templates/nextblock-template/app/[slug]/page.tsx +5 -0
  16. package/templates/nextblock-template/app/actions.ts +58 -8
  17. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +83 -0
  18. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +9 -9
  19. package/templates/nextblock-template/app/article/[slug]/page.tsx +5 -0
  20. package/templates/nextblock-template/app/cms/settings/security/actions.ts +30 -0
  21. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +69 -0
  22. package/templates/nextblock-template/app/layout.tsx +57 -3
  23. package/templates/nextblock-template/app/lib/site-settings.ts +22 -7
  24. package/templates/nextblock-template/app/page.tsx +6 -0
  25. package/templates/nextblock-template/app/product/[slug]/page.tsx +5 -0
  26. package/templates/nextblock-template/app/setup/SetupWizard.tsx +771 -0
  27. package/templates/nextblock-template/app/setup/layout.tsx +13 -0
  28. package/templates/nextblock-template/app/setup/page.tsx +103 -0
  29. package/templates/nextblock-template/components/AppShell.tsx +12 -0
  30. package/templates/nextblock-template/components/header-auth.tsx +24 -62
  31. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -0
  32. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -0
  33. package/templates/nextblock-template/docker/kong/kong.yml +112 -0
  34. package/templates/nextblock-template/docker/migrate/run-migrations.sh +51 -0
  35. package/templates/nextblock-template/docker-compose.yml +219 -0
  36. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -0
  37. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +67 -0
  38. package/templates/nextblock-template/docs/README.md +2 -0
  39. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +1 -1
  40. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +1 -1
  41. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +5 -5
  42. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +2 -2
  43. package/templates/nextblock-template/lib/setup/actions.ts +370 -0
  44. package/templates/nextblock-template/lib/setup/env-status.ts +86 -0
  45. package/templates/nextblock-template/lib/setup/env-write.ts +111 -0
  46. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -0
  47. package/templates/nextblock-template/lib/setup/schema-apply.ts +379 -0
  48. package/templates/nextblock-template/lib/setup/system-config.ts +105 -0
  49. package/templates/nextblock-template/lib/setup/types.ts +18 -0
  50. package/templates/nextblock-template/next.config.js +9 -0
  51. package/templates/nextblock-template/package.json +6 -2
  52. package/templates/nextblock-template/proxy.ts +143 -49
  53. package/templates/nextblock-template/scripts/docker-setup.mjs +242 -0
  54. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,13 @@
1
+ // Minimal layout for the first-boot wizard. Deliberately does NO Supabase work so it
2
+ // renders even on a completely unconfigured instance. (It is still nested inside the
3
+ // root layout, which short-circuits its own data loading when unconfigured.)
4
+ import type { ReactNode } from 'react';
5
+
6
+ export const metadata = {
7
+ title: 'Set up NextBlock',
8
+ robots: { index: false, follow: false },
9
+ };
10
+
11
+ export default function SetupLayout({ children }: { children: ReactNode }) {
12
+ return <div className="mx-auto w-full max-w-3xl px-4 py-10 sm:py-16">{children}</div>;
13
+ }
@@ -0,0 +1,103 @@
1
+ import { redirect } from 'next/navigation';
2
+ import { createClient } from '@nextblock-cms/db/server';
3
+ import {
4
+ detectChannel,
5
+ isLocalWritableEnv,
6
+ isSupabaseConfigured,
7
+ type DeployChannel,
8
+ } from '../../lib/setup/env-status';
9
+ import { getProvisioningStatus } from '../../lib/setup/provisioning';
10
+ import SetupWizard, { type StoragePrefill } from './SetupWizard';
11
+
12
+ // Always evaluate live: the wizard's whole job is to react to the current env state.
13
+ export const dynamic = 'force-dynamic';
14
+
15
+ function buildStoragePrefill(channel: DeployChannel, supabaseUrl: string): StoragePrefill {
16
+ if (channel === 'docker') {
17
+ // The Docker stack ships MinIO as the S3 backend; docker-setup wrote these already.
18
+ return {
19
+ kind: 'minio',
20
+ readOnly: true,
21
+ accountId: process.env.R2_ACCOUNT_ID ?? 'minio',
22
+ bucket: process.env.STORAGE_BUCKET ?? process.env.R2_BUCKET_NAME ?? 'media',
23
+ endpoint: process.env.R2_S3_ENDPOINT ?? 'http://minio:9000',
24
+ publicUrl: process.env.NEXT_PUBLIC_R2_PUBLIC_URL ?? 'http://localhost:9000',
25
+ baseUrl: process.env.NEXT_PUBLIC_R2_BASE_URL ?? '',
26
+ accessKeyId: '',
27
+ secretAccessKey: '',
28
+ };
29
+ }
30
+
31
+ if (channel === 'vercel') {
32
+ // One-click Vercel deploys connect to Supabase Storage's S3-compatible endpoint.
33
+ const base = supabaseUrl.replace(/\/$/, '');
34
+ return {
35
+ kind: 'supabase',
36
+ readOnly: false,
37
+ accountId: 'supabase',
38
+ bucket: 'media',
39
+ endpoint: base ? `${base}/storage/v1/s3` : '',
40
+ publicUrl: base ? `${base}/storage/v1/object/public` : '',
41
+ baseUrl: '',
42
+ accessKeyId: process.env.R2_ACCESS_KEY_ID ?? '',
43
+ secretAccessKey: '',
44
+ };
45
+ }
46
+
47
+ // Local: the user brings their own Cloudflare R2 (higher free-tier storage).
48
+ return {
49
+ kind: 'r2',
50
+ readOnly: false,
51
+ accountId: process.env.R2_ACCOUNT_ID ?? '',
52
+ bucket: process.env.R2_BUCKET_NAME ?? '',
53
+ endpoint: '',
54
+ publicUrl: process.env.NEXT_PUBLIC_R2_PUBLIC_URL ?? '',
55
+ baseUrl: process.env.NEXT_PUBLIC_R2_BASE_URL ?? '',
56
+ accessKeyId: process.env.R2_ACCESS_KEY_ID ?? '',
57
+ secretAccessKey: '',
58
+ };
59
+ }
60
+
61
+ export default async function SetupPage() {
62
+ const status = await getProvisioningStatus();
63
+
64
+ // Already finished — there's a first admin. Get out of the wizard.
65
+ if (status.hasAdmin) {
66
+ redirect('/cms/dashboard');
67
+ }
68
+
69
+ // A signed-in user never needs the wizard. This also closes the brief window right
70
+ // after completion where is_admin_created may still be read-stale: the just-created
71
+ // admin has a session, so send them straight to the CMS. (Only checked when
72
+ // configured — an unconfigured instance has no real auth.)
73
+ if (status.configured) {
74
+ const supabase = createClient();
75
+ const {
76
+ data: { user },
77
+ } = await supabase.auth.getUser();
78
+ if (user) {
79
+ redirect('/cms/dashboard');
80
+ }
81
+ }
82
+
83
+ const channel = detectChannel();
84
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? '';
85
+
86
+ return (
87
+ <SetupWizard
88
+ channel={channel}
89
+ configured={isSupabaseConfigured()}
90
+ writable={isLocalWritableEnv()}
91
+ siteUrl={process.env.NEXT_PUBLIC_URL ?? ''}
92
+ storagePrefill={buildStoragePrefill(channel, supabaseUrl)}
93
+ smtpPrefill={{
94
+ host: process.env.SMTP_HOST ?? '',
95
+ port: process.env.SMTP_PORT ?? '465',
96
+ user: process.env.SMTP_USER ?? '',
97
+ fromEmail: process.env.SMTP_FROM_EMAIL ?? '',
98
+ fromName: process.env.SMTP_FROM_NAME ?? '',
99
+ }}
100
+ turnstilePrefill={{ siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ?? '' }}
101
+ />
102
+ );
103
+ }
@@ -66,8 +66,20 @@ export function AppShell({
66
66
  }: AppShellProps) {
67
67
  const pathname = usePathname() || '';
68
68
  const isCmsRequest = pathname.startsWith('/cms');
69
+ const isSetupRequest = pathname === '/setup' || pathname.startsWith('/setup/');
69
70
  const branding = useMemo(() => ({ logo, siteTitle }), [logo, siteTitle]);
70
71
 
72
+ // The first-boot setup wizard renders on its own clean, chrome-free page: no header
73
+ // or footer, and crucially no EnvVarWarning — the whole point of /setup is to supply
74
+ // the very env vars that warning complains about.
75
+ if (isSetupRequest) {
76
+ return (
77
+ <AppBrandingContext.Provider value={branding}>
78
+ <main className="min-h-screen bg-background">{children}</main>
79
+ </AppBrandingContext.Provider>
80
+ );
81
+ }
82
+
71
83
  return (
72
84
  <AppBrandingContext.Provider value={branding}>
73
85
  {process.env.NEXT_PUBLIC_IS_SANDBOX === 'true' && !isCmsRequest && <SandboxBanner />}
@@ -1,70 +1,32 @@
1
1
  'use client';
2
2
 
3
- import { hasPublicEnvVars } from "@nextblock-cms/utils";
4
- import Link from "next/link";
5
- import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
6
- import { Badge } from "@nextblock-cms/ui";
7
- import { Button } from "@nextblock-cms/ui";
8
- import { signOutAction } from "../app/actions";
9
- import { useAuth } from "../context/AuthContext";
10
- import { useTranslations } from "@nextblock-cms/utils";
11
-
12
- import {
13
- DropdownMenu,
14
- DropdownMenuContent,
15
- DropdownMenuItem,
16
- DropdownMenuLabel,
17
- DropdownMenuSeparator,
18
- DropdownMenuTrigger,
19
- } from "@nextblock-cms/ui";
3
+ import Link from "next/link";
4
+ import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
5
+ import { Button } from "@nextblock-cms/ui";
6
+ import { signOutAction } from "../app/actions";
7
+ import { useAuth } from "../context/AuthContext";
8
+ import { useTranslations } from "@nextblock-cms/utils";
9
+
10
+ import {
11
+ DropdownMenu,
12
+ DropdownMenuContent,
13
+ DropdownMenuItem,
14
+ DropdownMenuLabel,
15
+ DropdownMenuSeparator,
16
+ DropdownMenuTrigger,
17
+ } from "@nextblock-cms/ui";
20
18
  import { User, LogOut, LayoutDashboard } from "lucide-react";
21
19
 
22
- export default function AuthButton() {
23
- const { user, profile, isAdmin, isWriter } = useAuth();
24
- const { t } = useTranslations();
25
- const displayName = profile?.full_name || profile?.github_username || user?.email || null;
26
- const showAdminLink = isAdmin || isWriter;
27
-
28
- const handleSignOut = async () => {
29
- await signOutAction();
30
- };
20
+ export default function AuthButton() {
21
+ const { user, profile, isAdmin, isWriter } = useAuth();
22
+ const { t } = useTranslations();
23
+ const displayName = profile?.full_name || profile?.github_username || user?.email || null;
24
+ const showAdminLink = isAdmin || isWriter;
25
+
26
+ const handleSignOut = async () => {
27
+ await signOutAction();
28
+ };
31
29
 
32
- if (!hasPublicEnvVars) {
33
- return (
34
- <>
35
- <div className="flex gap-4 items-center">
36
- <div>
37
- <Badge
38
- variant={"default"}
39
- className="font-normal pointer-events-none"
40
- >
41
- {t('update_env_file_warning')}
42
- </Badge>
43
- </div>
44
- <div className="flex gap-2">
45
- <Button
46
- asChild
47
- size="sm"
48
- variant={"outline"}
49
- disabled
50
- className="opacity-75 cursor-none pointer-events-none"
51
- >
52
- <Link href="/sign-in">{t('sign_in')}</Link>
53
- </Button>
54
- <Button
55
- asChild
56
- size="sm"
57
- variant={"default"}
58
- disabled
59
- className="opacity-75 cursor-none pointer-events-none"
60
- >
61
- <Link href="/sign-up">{t('sign_up')}</Link>
62
- </Button>
63
- </div>
64
- </div>
65
- </>
66
- );
67
- }
68
30
  return user ? (
69
31
  <div className="flex items-center gap-4">
70
32
  <DropdownMenu>
@@ -0,0 +1,6 @@
1
+ -- Expose the JWT secret to Postgres as a database GUC, matching Supabase's setup. Runs once.
2
+ \set jwt_secret `echo "$JWT_SECRET"`
3
+ \set jwt_exp `echo "$JWT_EXP"`
4
+
5
+ alter database postgres set "app.settings.jwt_secret" to :'jwt_secret';
6
+ alter database postgres set "app.settings.jwt_exp" to :'jwt_exp';
@@ -0,0 +1,25 @@
1
+ -- Self-hosted Supabase role passwords. The supabase/postgres image creates the platform roles;
2
+ -- GoTrue logs in as supabase_auth_admin and PostgREST as authenticator. Give the roles that exist
3
+ -- the generated POSTGRES_PASSWORD (the set varies by image tag, so each is guarded; the password
4
+ -- is passed via a session GUC because psql variables aren't interpolated inside a dollar block).
5
+ -- Runs once on first init.
6
+ \set pgpass `echo "$POSTGRES_PASSWORD"`
7
+ select set_config('nextblock.pgpass', :'pgpass', false);
8
+
9
+ do $$
10
+ declare
11
+ role_name text;
12
+ begin
13
+ foreach role_name in array array[
14
+ 'authenticator',
15
+ 'pgbouncer',
16
+ 'supabase_auth_admin',
17
+ 'supabase_storage_admin',
18
+ 'supabase_functions_admin',
19
+ 'supabase_read_only_user'
20
+ ] loop
21
+ if exists (select 1 from pg_roles where rolname = role_name) then
22
+ execute format('alter role %I with password %L', role_name, current_setting('nextblock.pgpass'));
23
+ end if;
24
+ end loop;
25
+ end $$;
@@ -0,0 +1,112 @@
1
+ # Kong declarative (DB-less) gateway config for the NextBlock self-hosted stack.
2
+ #
3
+ # $SUPABASE_ANON_KEY and $SUPABASE_SERVICE_KEY are substituted at container start by the kong
4
+ # service entrypoint (docker-compose.yml) from the generated ANON_KEY / SERVICE_ROLE_KEY env.
5
+ # Trimmed to just the Auth (GoTrue) and PostgREST routes the app uses.
6
+ _format_version: '2.1'
7
+ _transform: true
8
+
9
+ consumers:
10
+ - username: anon
11
+ keyauth_credentials:
12
+ - key: $SUPABASE_ANON_KEY
13
+ - username: service_role
14
+ keyauth_credentials:
15
+ - key: $SUPABASE_SERVICE_KEY
16
+
17
+ acls:
18
+ - consumer: anon
19
+ group: anon
20
+ - consumer: service_role
21
+ group: admin
22
+
23
+ services:
24
+ - name: auth-v1-open
25
+ url: http://auth:9999/verify
26
+ routes:
27
+ - name: auth-v1-open
28
+ strip_path: true
29
+ paths:
30
+ - /auth/v1/verify
31
+ plugins:
32
+ - name: cors
33
+ - name: auth-v1-open-callback
34
+ url: http://auth:9999/callback
35
+ routes:
36
+ - name: auth-v1-open-callback
37
+ strip_path: true
38
+ paths:
39
+ - /auth/v1/callback
40
+ plugins:
41
+ - name: cors
42
+ - name: auth-v1-open-authorize
43
+ url: http://auth:9999/authorize
44
+ routes:
45
+ - name: auth-v1-open-authorize
46
+ strip_path: true
47
+ paths:
48
+ - /auth/v1/authorize
49
+ plugins:
50
+ - name: cors
51
+
52
+ - name: auth-v1
53
+ url: http://auth:9999/
54
+ routes:
55
+ - name: auth-v1-all
56
+ strip_path: true
57
+ paths:
58
+ - /auth/v1/
59
+ plugins:
60
+ - name: cors
61
+ - name: key-auth
62
+ config:
63
+ hide_credentials: false
64
+ - name: acl
65
+ config:
66
+ hide_groups_header: true
67
+ allow:
68
+ - admin
69
+ - anon
70
+
71
+ - name: rest-v1
72
+ url: http://rest:3000/
73
+ routes:
74
+ - name: rest-v1-all
75
+ strip_path: true
76
+ paths:
77
+ - /rest/v1/
78
+ plugins:
79
+ - name: cors
80
+ - name: key-auth
81
+ config:
82
+ hide_credentials: true
83
+ - name: acl
84
+ config:
85
+ hide_groups_header: true
86
+ allow:
87
+ - admin
88
+ - anon
89
+
90
+ - name: graphql-v1
91
+ url: http://rest:3000/rpc/graphql
92
+ routes:
93
+ - name: graphql-v1-all
94
+ strip_path: true
95
+ paths:
96
+ - /graphql/v1
97
+ plugins:
98
+ - name: cors
99
+ - name: key-auth
100
+ config:
101
+ hide_credentials: false
102
+ - name: request-transformer
103
+ config:
104
+ add:
105
+ headers:
106
+ - 'Content-Profile: graphql_public'
107
+ - name: acl
108
+ config:
109
+ hide_groups_header: true
110
+ allow:
111
+ - admin
112
+ - anon
@@ -0,0 +1,51 @@
1
+ #!/bin/sh
2
+ # Applies the NextBlock SQL migrations (mounted at /migrations) in chronological filename order,
3
+ # once GoTrue has provisioned the auth schema (profiles etc. FK to auth.users). Idempotent: a
4
+ # tracking table records applied versions so restarts never re-run a migration. Each file runs
5
+ # in a single transaction (ON_ERROR_STOP).
6
+ set -eu
7
+
8
+ PGHOST="${POSTGRES_HOST:-db}"
9
+ PGPORT="${POSTGRES_PORT:-5432}"
10
+ PGUSER="postgres"
11
+ PGDATABASE="${POSTGRES_DB:-postgres}"
12
+ export PGPASSWORD="${POSTGRES_PASSWORD}"
13
+
14
+ psql_cmd() {
15
+ psql -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" "$@"
16
+ }
17
+
18
+ echo "[migrate] waiting for Postgres at ${PGHOST}:${PGPORT}..."
19
+ until psql_cmd -c 'select 1;' >/dev/null 2>&1; do
20
+ sleep 2
21
+ done
22
+
23
+ echo "[migrate] waiting for GoTrue to create auth.users..."
24
+ until [ "$(psql_cmd -tAc "select to_regclass('auth.users') is not null;")" = "t" ]; do
25
+ sleep 2
26
+ done
27
+
28
+ psql_cmd -c "create table if not exists public._nextblock_docker_migrations (
29
+ version text primary key,
30
+ applied_at timestamptz not null default now()
31
+ );"
32
+
33
+ applied_any=0
34
+ for file in $(ls /migrations/*.sql | sort); do
35
+ version="$(basename "$file" .sql)"
36
+ already="$(psql_cmd -tAc "select 1 from public._nextblock_docker_migrations where version = '${version}';")"
37
+ if [ "$already" = "1" ]; then
38
+ echo "[migrate] skip ${version} (already applied)"
39
+ continue
40
+ fi
41
+ echo "[migrate] applying ${version}"
42
+ psql_cmd --single-transaction -f "$file"
43
+ psql_cmd -c "insert into public._nextblock_docker_migrations (version) values ('${version}');"
44
+ applied_any=1
45
+ done
46
+
47
+ if [ "$applied_any" = "1" ]; then
48
+ echo "[migrate] migrations applied successfully."
49
+ else
50
+ echo "[migrate] database already up to date."
51
+ fi
@@ -0,0 +1,219 @@
1
+ # Self-hosted NextBlock CMS stack for a standalone project. Generated/driven by
2
+ # `npm run docker:setup`, which writes the .env this file reads. Mirrors the production Supabase
3
+ # topology: Postgres + GoTrue + PostgREST behind a Kong gateway, plus MinIO for S3 media.
4
+ name: nextblock
5
+
6
+ services:
7
+ db:
8
+ image: ${SUPABASE_DB_IMAGE:-supabase/postgres:15.8.1.085}
9
+ restart: unless-stopped
10
+ environment:
11
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
12
+ POSTGRES_DB: ${POSTGRES_DB:-postgres}
13
+ JWT_SECRET: ${JWT_SECRET}
14
+ JWT_EXP: ${JWT_EXP:-3600}
15
+ volumes:
16
+ - nextblock_db_store:/var/lib/postgresql/data
17
+ - ./docker/db/init/99-roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro
18
+ - ./docker/db/init/99-jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro
19
+ healthcheck:
20
+ test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', 'localhost']
21
+ interval: 5s
22
+ timeout: 5s
23
+ retries: 12
24
+ # supabase/postgres runs a heavy first-init; give it time before counting health failures.
25
+ start_period: 60s
26
+ ports:
27
+ - '${POSTGRES_PORT_EXTERNAL:-54322}:5432'
28
+
29
+ auth:
30
+ image: ${SUPABASE_GOTRUE_IMAGE:-supabase/gotrue:v2.189.0}
31
+ restart: unless-stopped
32
+ depends_on:
33
+ db:
34
+ condition: service_healthy
35
+ environment:
36
+ GOTRUE_API_HOST: 0.0.0.0
37
+ GOTRUE_API_PORT: 9999
38
+ API_EXTERNAL_URL: ${API_EXTERNAL_URL:-http://localhost:8000}
39
+ GOTRUE_DB_DRIVER: postgres
40
+ GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-postgres}
41
+ GOTRUE_SITE_URL: ${SITE_URL:-http://localhost:3000}
42
+ GOTRUE_URI_ALLOW_LIST: '*'
43
+ GOTRUE_DISABLE_SIGNUP: 'false'
44
+ GOTRUE_JWT_ADMIN_ROLES: service_role
45
+ GOTRUE_JWT_AUD: authenticated
46
+ GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
47
+ GOTRUE_JWT_EXP: ${JWT_EXP:-3600}
48
+ GOTRUE_JWT_SECRET: ${JWT_SECRET}
49
+ GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL:-http://localhost:8000}/auth/v1
50
+ GOTRUE_EXTERNAL_EMAIL_ENABLED: 'true'
51
+ GOTRUE_MAILER_AUTOCONFIRM: ${GOTRUE_MAILER_AUTOCONFIRM:-true}
52
+ GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_FROM_EMAIL:-admin@example.com}
53
+ GOTRUE_SMTP_HOST: ${SMTP_HOST:-}
54
+ # Must be a valid integer even when SMTP is skipped (GoTrue parses it as int and crashes on
55
+ # ''). With no host + autoconfirm on, no mail is sent, so the value is otherwise inert.
56
+ GOTRUE_SMTP_PORT: ${SMTP_PORT:-2500}
57
+ GOTRUE_SMTP_USER: ${SMTP_USER:-}
58
+ GOTRUE_SMTP_PASS: ${SMTP_PASS:-}
59
+ GOTRUE_SMTP_SENDER_NAME: ${SMTP_FROM_NAME:-NextBlock}
60
+ GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
61
+ GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
62
+ GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify
63
+ GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
64
+ healthcheck:
65
+ test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:9999/health']
66
+ interval: 5s
67
+ timeout: 5s
68
+ retries: 15
69
+
70
+ rest:
71
+ image: ${SUPABASE_POSTGREST_IMAGE:-postgrest/postgrest:v12.2.12}
72
+ restart: unless-stopped
73
+ depends_on:
74
+ db:
75
+ condition: service_healthy
76
+ environment:
77
+ PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-postgres}
78
+ PGRST_DB_SCHEMAS: public,graphql_public
79
+ PGRST_DB_ANON_ROLE: anon
80
+ PGRST_JWT_SECRET: ${JWT_SECRET}
81
+ PGRST_DB_USE_LEGACY_GUCS: 'false'
82
+ PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
83
+ PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXP:-3600}
84
+ PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
85
+
86
+ kong:
87
+ image: ${SUPABASE_KONG_IMAGE:-kong:2.8.1}
88
+ restart: unless-stopped
89
+ depends_on:
90
+ auth:
91
+ condition: service_started
92
+ rest:
93
+ condition: service_started
94
+ environment:
95
+ KONG_DATABASE: 'off'
96
+ KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
97
+ KONG_DNS_ORDER: LAST,A,CNAME
98
+ KONG_PLUGINS: request-transformer,cors,key-auth,acl
99
+ KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
100
+ KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
101
+ # localhost cookies aren't port-scoped, so the app's Supabase auth cookies are also sent to
102
+ # Kong on every supabase-js request — give nginx room so they don't 431.
103
+ KONG_NGINX_HTTP_LARGE_CLIENT_HEADER_BUFFERS: 8 32k
104
+ SUPABASE_ANON_KEY: ${ANON_KEY}
105
+ SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
106
+ volumes:
107
+ - ./docker/kong/kong.yml:/home/kong/temp.yml:ro
108
+ entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
109
+ ports:
110
+ - '${KONG_HTTP_PORT:-8000}:8000'
111
+
112
+ minio:
113
+ image: ${MINIO_IMAGE:-minio/minio:latest}
114
+ restart: unless-stopped
115
+ command: server /data --console-address ":9001"
116
+ environment:
117
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER}
118
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
119
+ volumes:
120
+ - nextblock_media:/data
121
+ ports:
122
+ - '${MINIO_S3_PORT:-9000}:9000'
123
+ - '${MINIO_CONSOLE_PORT:-9001}:9001'
124
+
125
+ minio-init:
126
+ image: ${MINIO_MC_IMAGE:-minio/mc:latest}
127
+ restart: 'no'
128
+ depends_on:
129
+ minio:
130
+ condition: service_started
131
+ environment:
132
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER}
133
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
134
+ STORAGE_BUCKET: ${STORAGE_BUCKET:-nextblock}
135
+ entrypoint: >
136
+ /bin/sh -c "
137
+ until mc alias set local http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD; do echo 'waiting for minio...'; sleep 2; done &&
138
+ mc mb --ignore-existing local/$$STORAGE_BUCKET &&
139
+ mc anonymous set download local/$$STORAGE_BUCKET &&
140
+ echo 'minio bucket ready'
141
+ "
142
+
143
+ migrate:
144
+ image: ${MIGRATE_IMAGE:-postgres:15-alpine}
145
+ restart: 'no'
146
+ depends_on:
147
+ db:
148
+ condition: service_healthy
149
+ auth:
150
+ condition: service_healthy
151
+ environment:
152
+ POSTGRES_HOST: db
153
+ POSTGRES_PORT: 5432
154
+ POSTGRES_DB: ${POSTGRES_DB:-postgres}
155
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
156
+ volumes:
157
+ - ./supabase/migrations:/migrations:ro
158
+ - ./docker/migrate/run-migrations.sh:/run-migrations.sh:ro
159
+ entrypoint: ['sh', '/run-migrations.sh']
160
+
161
+ nextblock-cms:
162
+ build:
163
+ context: .
164
+ dockerfile: Dockerfile
165
+ args:
166
+ NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
167
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
168
+ NEXT_PUBLIC_URL: ${NEXT_PUBLIC_URL:-http://localhost:3000}
169
+ NEXT_PUBLIC_R2_PUBLIC_URL: ${NEXT_PUBLIC_R2_PUBLIC_URL}
170
+ NEXT_PUBLIC_R2_BASE_URL: ${NEXT_PUBLIC_R2_BASE_URL}
171
+ NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-}
172
+ NEXT_PUBLIC_IS_SANDBOX: ${NEXT_PUBLIC_IS_SANDBOX:-true}
173
+ restart: unless-stopped
174
+ depends_on:
175
+ migrate:
176
+ condition: service_completed_successfully
177
+ kong:
178
+ condition: service_started
179
+ minio-init:
180
+ condition: service_completed_successfully
181
+ environment:
182
+ NODE_ENV: production
183
+ PORT: 3000
184
+ HOSTNAME: 0.0.0.0
185
+ # Browser AND server both use localhost:8000; the in-container loopback proxy (see Dockerfile)
186
+ # forwards it to Kong, keeping the host-derived Supabase auth cookie key consistent for SSR.
187
+ NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
188
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
189
+ SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
190
+ NEXT_PUBLIC_URL: ${NEXT_PUBLIC_URL:-http://localhost:3000}
191
+ NEXT_PUBLIC_IS_SANDBOX: ${NEXT_PUBLIC_IS_SANDBOX:-true}
192
+ CRON_SECRET: ${CRON_SECRET}
193
+ DRAFT_MODE_SECRET: ${DRAFT_MODE_SECRET}
194
+ REVALIDATE_SECRET_TOKEN: ${REVALIDATE_SECRET_TOKEN}
195
+ CORTEX_AI_ENCRYPTION_KEY: ${CORTEX_AI_ENCRYPTION_KEY:-}
196
+ R2_ACCOUNT_ID: ${R2_ACCOUNT_ID:-minio}
197
+ R2_BUCKET_NAME: ${STORAGE_BUCKET:-nextblock}
198
+ R2_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
199
+ R2_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
200
+ R2_REGION: ${R2_REGION:-us-east-1}
201
+ R2_S3_ENDPOINT: ${R2_S3_ENDPOINT:-http://minio:9000}
202
+ R2_S3_PUBLIC_ENDPOINT: ${R2_S3_PUBLIC_ENDPOINT:-http://localhost:9000}
203
+ R2_FORCE_PATH_STYLE: 'true'
204
+ NEXT_PUBLIC_R2_PUBLIC_URL: ${NEXT_PUBLIC_R2_PUBLIC_URL}
205
+ NEXT_PUBLIC_R2_BASE_URL: ${NEXT_PUBLIC_R2_BASE_URL}
206
+ SMTP_HOST: ${SMTP_HOST:-}
207
+ SMTP_PORT: ${SMTP_PORT:-}
208
+ SMTP_USER: ${SMTP_USER:-}
209
+ SMTP_PASS: ${SMTP_PASS:-}
210
+ SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
211
+ SMTP_FROM_NAME: ${SMTP_FROM_NAME:-}
212
+ TURNSTILE_SECRET_KEY: ${TURNSTILE_SECRET_KEY:-}
213
+ NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-}
214
+ ports:
215
+ - '${APP_PORT:-3000}:3000'
216
+
217
+ volumes:
218
+ nextblock_db_store:
219
+ nextblock_media: