create-nextblock 0.8.10 → 0.9.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.
@@ -83,9 +83,30 @@ async function handleCommand(projectDirectory, options) {
83
83
  try {
84
84
  console.log(chalk.bold.cyan(`\n🧱 create-nextblock v${CLI_VERSION}\n`));
85
85
 
86
+ // Pick the hosting profile up front (interactive only). Cloud = Vercel + Supabase Cloud;
87
+ // Docker = a fully local self-hosted sandbox that needs no cloud accounts.
88
+ let hostingMode = 'cloud';
89
+ if (!yes) {
90
+ const modeChoice = await clack.select({
91
+ message: 'Select your target hosting environment profile:',
92
+ options: [
93
+ { value: 'cloud', label: 'Managed Cloud Mode (Vercel + Supabase Cloud)' },
94
+ {
95
+ value: 'docker',
96
+ label: 'Local Self-Hosted Docker Mode (One-Click Local Sandbox)',
97
+ },
98
+ ],
99
+ initialValue: 'cloud',
100
+ });
101
+ if (clack.isCancel(modeChoice)) {
102
+ handleWizardCancel('Setup cancelled.');
103
+ }
104
+ hostingMode = modeChoice;
105
+ }
106
+
86
107
  // Prerequisites gate (interactive only) — shown BEFORE we ask for a name, scaffold, or
87
108
  // install, so anyone who isn't ready can cancel without creating anything.
88
- if (!yes) {
109
+ if (!yes && hostingMode === 'cloud') {
89
110
  clack.note(
90
111
  [
91
112
  '1. A Supabase project https://supabase.com/dashboard',
@@ -119,6 +140,35 @@ async function handleCommand(projectDirectory, options) {
119
140
  );
120
141
  return;
121
142
  }
143
+ } else if (!yes && hostingMode === 'docker') {
144
+ clack.note(
145
+ [
146
+ 'Local Self-Hosted Docker Mode runs everything on your machine — no cloud accounts needed.',
147
+ '',
148
+ 'Requirement:',
149
+ ' • Docker Desktop installed and running (https://www.docker.com/products/docker-desktop)',
150
+ '',
151
+ 'Optional (you can skip both at the prompts):',
152
+ ' • Cloudflare Turnstile keys (bot protection)',
153
+ ' • SMTP credentials (otherwise sign-ups auto-confirm with no email)',
154
+ ].join('\n'),
155
+ 'One-click local sandbox',
156
+ );
157
+
158
+ const ready = await clack.confirm({
159
+ message: 'Is Docker Desktop installed and running?',
160
+ initialValue: true,
161
+ });
162
+ if (clack.isCancel(ready)) {
163
+ handleWizardCancel('Setup cancelled.');
164
+ }
165
+ if (!ready) {
166
+ clack.note(
167
+ 'No problem — nothing was created. Install & start Docker Desktop, then run\n`npm create nextblock` again.',
168
+ 'Come back when ready',
169
+ );
170
+ return;
171
+ }
122
172
  }
123
173
 
124
174
  let projectName = projectDirectory;
@@ -214,11 +264,15 @@ async function handleCommand(projectDirectory, options) {
214
264
  console.log(chalk.yellow('Skipping dependency installation.'));
215
265
  }
216
266
 
217
- // Run the setup wizard after dependencies are installed so package assets are available.
218
- // When it runs, its own "next steps" outro (cd + npm run dev) is the final message, so we
219
- // don't print a second closing block here — the whole flow completes in this one command.
267
+ // Run the setup flow after dependencies are installed so package assets are available.
268
+ // When it runs, its own "next steps" outro is the final message, so we don't print a second
269
+ // closing block here — the whole flow completes in this one command.
220
270
  if (!yes) {
221
- await runSetupWizard(projectDir, projectName);
271
+ if (hostingMode === 'docker') {
272
+ await runDockerSetup(projectDir, projectName);
273
+ } else {
274
+ await runSetupWizard(projectDir, projectName);
275
+ }
222
276
  } else {
223
277
  // Non-interactive path: nothing was configured, so point the user at their env file.
224
278
  console.log(
@@ -898,6 +952,34 @@ async function runSetupWizard(projectDir, projectName) {
898
952
  );
899
953
  }
900
954
 
955
+ // Local Self-Hosted Docker Mode: materialize the supabase migrations out of the installed
956
+ // @nextblock-cms/db package (the migration-runner container applies them on boot), then hand off
957
+ // to the project's own zero-dependency Docker setup script (prompts + .env + `docker compose up`).
958
+ async function runDockerSetup(projectDir, projectName) {
959
+ const projectPath = resolve(projectDir);
960
+ process.chdir(projectPath);
961
+
962
+ clack.intro('🐳 NextBlock™ CMS — Local Self-Hosted Docker setup');
963
+
964
+ await ensureSupabaseAssets(projectPath, { required: true });
965
+
966
+ const setupScript = resolve(projectPath, 'scripts', 'docker-setup.mjs');
967
+ if (!(await fs.pathExists(setupScript))) {
968
+ clack.note(
969
+ 'scripts/docker-setup.mjs is missing from the template. Run `npm run sync:create-nextblock` and try again.',
970
+ 'Docker setup unavailable',
971
+ );
972
+ return;
973
+ }
974
+
975
+ // The script drives docker compose interactively; inherit stdio so its prompts work.
976
+ await runCommand('node', ['scripts/docker-setup.mjs'], { cwd: projectPath });
977
+
978
+ clack.outro(
979
+ `🎉 Your NextBlock™ project ${projectName ? `"${projectName}" ` : ''}is running in Docker.\nApp: http://localhost:3000 (first sign-up becomes ADMIN)`,
980
+ );
981
+ }
982
+
901
983
  async function configureHostedSupabaseAuth(
902
984
  projectDir,
903
985
  { projectId, siteUrl, accessToken, smtpValues },
@@ -1924,7 +2006,11 @@ function buildNextConfigContent(editorUtilNames) {
1924
2006
  '/**',
1925
2007
  " * @type {import('next').NextConfig}",
1926
2008
  ' **/',
2009
+ // Self-hosted Docker builds emit a standalone server (`node server.js`); gated on
2010
+ // DOCKER_BUILD so a normal `next build` / Vercel deploy is unaffected.
2011
+ "const isDockerStandalone = process.env.DOCKER_BUILD === 'true';",
1927
2012
  'const nextConfig = {',
2013
+ " ...(isDockerStandalone ? { output: 'standalone' } : {}),",
1928
2014
  ' outputFileTracingRoot: path.join(__dirname),',
1929
2015
  ' env: {',
1930
2016
  ' NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,',
@@ -0,0 +1,23 @@
1
+ # Keep the build context lean; deps are reinstalled in the image and secrets stay out.
2
+ node_modules
3
+ .git
4
+ .github
5
+ .next
6
+ out
7
+ dist
8
+ coverage
9
+ tmp
10
+ backup
11
+ backups
12
+ *.log
13
+ npm-debug.log*
14
+
15
+ # Secrets never enter the image (NEXT_PUBLIC_* arrive as build args).
16
+ .env
17
+ .env.*
18
+ !.env.docker.example
19
+
20
+ # Not needed inside the app image (mounted/used by other compose services instead).
21
+ docker
22
+ supabase/.branches
23
+ supabase/.temp
@@ -0,0 +1,55 @@
1
+ # ──────────────────────────────────────────────────────────────────────────────
2
+ # Reference for the Docker (self-hosted) .env that `npm run docker:setup` generates.
3
+ # You normally do NOT edit this by hand — run `npm run docker:setup` and it writes a
4
+ # `.env` with secure random secrets. This file documents every key the stack reads.
5
+ # ──────────────────────────────────────────────────────────────────────────────
6
+
7
+ # --- Auto-generated secrets (crypto-random) --------------------------------------------------
8
+ POSTGRES_PASSWORD=replace-me-generated
9
+ JWT_SECRET=replace-me-at-least-32-chars
10
+ JWT_EXP=3600
11
+ # anon + service_role keys are HS256 JWTs SIGNED with JWT_SECRET (generated for you):
12
+ ANON_KEY=replace-me-anon-jwt
13
+ SERVICE_ROLE_KEY=replace-me-service-role-jwt
14
+ CRON_SECRET=replace-me-generated
15
+ DRAFT_MODE_SECRET=replace-me-generated
16
+ REVALIDATE_SECRET_TOKEN=replace-me-generated
17
+
18
+ # --- Supabase wiring (internal container network vs browser) ---------------------------------
19
+ NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
20
+ SUPABASE_INTERNAL_URL=http://kong:8000
21
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY}
22
+ SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
23
+ API_EXTERNAL_URL=http://localhost:8000
24
+ SITE_URL=http://localhost:3000
25
+ POSTGRES_DB=postgres
26
+
27
+ # --- App ------------------------------------------------------------------------------------
28
+ NEXT_PUBLIC_URL=http://localhost:3000
29
+ NEXT_PUBLIC_IS_SANDBOX=true
30
+ CORTEX_AI_ENCRYPTION_KEY=
31
+
32
+ # --- MinIO object storage (S3-compatible) ----------------------------------------------------
33
+ MINIO_ROOT_USER=nextblock
34
+ MINIO_ROOT_PASSWORD=replace-me-generated
35
+ STORAGE_BUCKET=nextblock
36
+ R2_ACCOUNT_ID=minio
37
+ R2_REGION=us-east-1
38
+ R2_S3_ENDPOINT=http://minio:9000
39
+ R2_S3_PUBLIC_ENDPOINT=http://localhost:9000
40
+ R2_FORCE_PATH_STYLE=true
41
+ NEXT_PUBLIC_R2_BASE_URL=http://localhost:9000/nextblock
42
+ NEXT_PUBLIC_R2_PUBLIC_URL=http://localhost:9000/nextblock
43
+
44
+ # --- Cloudflare Turnstile (optional). Press Enter at setup to skip and use the dev sandbox. ---
45
+ NEXT_PUBLIC_TURNSTILE_SITE_KEY=
46
+ TURNSTILE_SECRET_KEY=
47
+
48
+ # --- SMTP (optional). Blank => GoTrue auto-confirms sign-ups (loopback mode). -----------------
49
+ GOTRUE_MAILER_AUTOCONFIRM=true
50
+ SMTP_HOST=
51
+ SMTP_PORT=
52
+ SMTP_USER=
53
+ SMTP_PASS=
54
+ SMTP_FROM_EMAIL=
55
+ SMTP_FROM_NAME=
@@ -0,0 +1,68 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ #
3
+ # Production image for a standalone NextBlock CMS project (single Next.js app, not a monorepo).
4
+ # Built with Next.js standalone output, gated on DOCKER_BUILD so a normal `npm run build` / a
5
+ # Vercel deploy is unaffected. Driven by docker-compose.yml (`npm run docker:setup`).
6
+
7
+ ###############################################################################
8
+ # Stage 1 — deps
9
+ ###############################################################################
10
+ FROM node:22-alpine AS deps
11
+ WORKDIR /app
12
+ RUN apk add --no-cache libc6-compat
13
+ COPY package.json ./
14
+ RUN npm install --no-audit --no-fund
15
+
16
+ ###############################################################################
17
+ # Stage 2 — builder (Next.js standalone output)
18
+ ###############################################################################
19
+ FROM node:22-alpine AS builder
20
+ WORKDIR /app
21
+ RUN apk add --no-cache libc6-compat
22
+ ENV NEXT_TELEMETRY_DISABLED=1 \
23
+ CI=true \
24
+ DOCKER_BUILD=true \
25
+ NODE_ENV=production
26
+ COPY --from=deps /app/node_modules ./node_modules
27
+ COPY . .
28
+
29
+ # NEXT_PUBLIC_* values are inlined into the browser bundle at build time. Server-only secrets are
30
+ # NOT baked in — they arrive at runtime from docker-compose.yml.
31
+ ARG NEXT_PUBLIC_SUPABASE_URL
32
+ ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
33
+ ARG NEXT_PUBLIC_URL
34
+ ARG NEXT_PUBLIC_R2_PUBLIC_URL
35
+ ARG NEXT_PUBLIC_R2_BASE_URL
36
+ ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
37
+ ARG NEXT_PUBLIC_IS_SANDBOX
38
+ ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \
39
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY \
40
+ NEXT_PUBLIC_URL=$NEXT_PUBLIC_URL \
41
+ NEXT_PUBLIC_R2_PUBLIC_URL=$NEXT_PUBLIC_R2_PUBLIC_URL \
42
+ NEXT_PUBLIC_R2_BASE_URL=$NEXT_PUBLIC_R2_BASE_URL \
43
+ NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY \
44
+ NEXT_PUBLIC_IS_SANDBOX=$NEXT_PUBLIC_IS_SANDBOX
45
+
46
+ RUN npm run build
47
+
48
+ ###############################################################################
49
+ # Stage 3 — runner (hardened, non-root)
50
+ ###############################################################################
51
+ FROM node:22-alpine AS runner
52
+ WORKDIR /app
53
+ RUN apk add --no-cache libc6-compat \
54
+ && addgroup -g 1001 -S nodejs \
55
+ && adduser -u 1001 -S nextjs -G nodejs
56
+ ENV NODE_ENV=production \
57
+ NEXT_TELEMETRY_DISABLED=1 \
58
+ PORT=3000 \
59
+ HOSTNAME=0.0.0.0
60
+
61
+ # A single-app project traces to its own root, so server.js sits at the top of .next/standalone.
62
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
63
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
64
+ COPY --from=builder --chown=nextjs:nodejs /app/public ./public
65
+
66
+ USER nextjs
67
+ EXPOSE 3000
68
+ CMD ["node", "server.js"]
@@ -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