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.
- package/bin/create-nextblock.js +101 -35
- package/docker-template/.dockerignore +23 -0
- package/docker-template/.env.docker.example +56 -0
- package/docker-template/Dockerfile +85 -0
- package/docker-template/docker/db/init/99-jwt.sql +6 -0
- package/docker-template/docker/db/init/99-roles.sql +25 -0
- package/docker-template/docker/kong/kong.yml +112 -0
- package/docker-template/docker/migrate/run-migrations.sh +51 -0
- package/docker-template/docker-compose.yml +219 -0
- package/docker-template/scripts/docker-setup.mjs +242 -0
- package/package.json +1 -1
- package/scripts/sync-template.js +29 -0
- package/templates/nextblock-template/.dockerignore +23 -0
- package/templates/nextblock-template/Dockerfile +85 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +5 -0
- package/templates/nextblock-template/app/actions.ts +58 -8
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +83 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +9 -9
- package/templates/nextblock-template/app/article/[slug]/page.tsx +5 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +30 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +69 -0
- package/templates/nextblock-template/app/layout.tsx +57 -3
- package/templates/nextblock-template/app/lib/site-settings.ts +22 -7
- package/templates/nextblock-template/app/page.tsx +6 -0
- package/templates/nextblock-template/app/product/[slug]/page.tsx +5 -0
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +771 -0
- package/templates/nextblock-template/app/setup/layout.tsx +13 -0
- package/templates/nextblock-template/app/setup/page.tsx +103 -0
- package/templates/nextblock-template/components/AppShell.tsx +12 -0
- package/templates/nextblock-template/components/header-auth.tsx +24 -62
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -0
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -0
- package/templates/nextblock-template/docker/kong/kong.yml +112 -0
- package/templates/nextblock-template/docker/migrate/run-migrations.sh +51 -0
- package/templates/nextblock-template/docker-compose.yml +219 -0
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -0
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +67 -0
- package/templates/nextblock-template/docs/README.md +2 -0
- package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +1 -1
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +1 -1
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +5 -5
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +2 -2
- package/templates/nextblock-template/lib/setup/actions.ts +370 -0
- package/templates/nextblock-template/lib/setup/env-status.ts +86 -0
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -0
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -0
- package/templates/nextblock-template/lib/setup/schema-apply.ts +379 -0
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -0
- package/templates/nextblock-template/lib/setup/types.ts +18 -0
- package/templates/nextblock-template/next.config.js +9 -0
- package/templates/nextblock-template/package.json +6 -2
- package/templates/nextblock-template/proxy.ts +143 -49
- package/templates/nextblock-template/scripts/docker-setup.mjs +242 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
package/bin/create-nextblock.js
CHANGED
|
@@ -83,30 +83,46 @@ 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
|
-
//
|
|
87
|
-
//
|
|
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';
|
|
88
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
|
+
|
|
107
|
+
// Cloud / local configuration moved to the browser First-Boot Setup Wizard (/setup), so the
|
|
108
|
+
// CLI no longer asks for Supabase / R2 / SMTP credentials here. Docker still preflights below.
|
|
109
|
+
if (!yes && hostingMode === 'docker') {
|
|
89
110
|
clack.note(
|
|
90
111
|
[
|
|
91
|
-
'
|
|
92
|
-
' ⢠Reference ID ā Project Settings > General > "Reference ID"',
|
|
93
|
-
' ⢠Connection string ā Connect (top bar) > Direct connection > URI',
|
|
94
|
-
' ⢠anon + service_role keys ā Project Settings > API Keys',
|
|
95
|
-
' ⢠Personal Access Token ā Account > Access Tokens > Generate new token',
|
|
112
|
+
'Local Self-Hosted Docker Mode runs everything on your machine ā no cloud accounts needed.',
|
|
96
113
|
'',
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
' ⢠Create an R2 API token (Object Read & Write); copy the Access Key ID + Secret (shown once)',
|
|
114
|
+
'Requirement:',
|
|
115
|
+
' ⢠Docker Desktop installed and running (https://www.docker.com/products/docker-desktop)',
|
|
100
116
|
'',
|
|
101
|
-
'
|
|
102
|
-
'
|
|
117
|
+
'Optional (you can skip both at the prompts):',
|
|
118
|
+
' ⢠Cloudflare Turnstile keys (bot protection)',
|
|
119
|
+
' ⢠SMTP credentials (otherwise sign-ups auto-confirm with no email)',
|
|
103
120
|
].join('\n'),
|
|
104
|
-
'
|
|
121
|
+
'One-click local sandbox',
|
|
105
122
|
);
|
|
106
123
|
|
|
107
124
|
const ready = await clack.confirm({
|
|
108
|
-
message:
|
|
109
|
-
'Do you have your Supabase, Cloudflare R2, and SMTP details ready?',
|
|
125
|
+
message: 'Is Docker Desktop installed and running?',
|
|
110
126
|
initialValue: true,
|
|
111
127
|
});
|
|
112
128
|
if (clack.isCancel(ready)) {
|
|
@@ -114,7 +130,7 @@ async function handleCommand(projectDirectory, options) {
|
|
|
114
130
|
}
|
|
115
131
|
if (!ready) {
|
|
116
132
|
clack.note(
|
|
117
|
-
'No problem ā nothing was created.
|
|
133
|
+
'No problem ā nothing was created. Install & start Docker Desktop, then run\n`npm create nextblock` again.',
|
|
118
134
|
'Come back when ready',
|
|
119
135
|
);
|
|
120
136
|
return;
|
|
@@ -214,26 +230,13 @@ async function handleCommand(projectDirectory, options) {
|
|
|
214
230
|
console.log(chalk.yellow('Skipping dependency installation.'));
|
|
215
231
|
}
|
|
216
232
|
|
|
217
|
-
// Run the
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
if (!yes) {
|
|
221
|
-
await
|
|
233
|
+
// Run the post-scaffold flow after dependencies are installed so package assets are available.
|
|
234
|
+
// Docker boots the local stack; everything else just materializes Supabase assets and points
|
|
235
|
+
// the user at the browser First-Boot Setup Wizard at /setup (no terminal credential prompts).
|
|
236
|
+
if (!yes && hostingMode === 'docker') {
|
|
237
|
+
await runDockerSetup(projectDir, projectName);
|
|
222
238
|
} else {
|
|
223
|
-
|
|
224
|
-
console.log(
|
|
225
|
-
chalk.green(
|
|
226
|
-
`\nSuccess! Your NextBlock⢠CMS project "${projectName}" is scaffolded.\n`,
|
|
227
|
-
),
|
|
228
|
-
);
|
|
229
|
-
console.log(chalk.cyan('Next steps:'));
|
|
230
|
-
console.log(chalk.cyan(` 1. cd ${projectName}`));
|
|
231
|
-
console.log(
|
|
232
|
-
chalk.gray(
|
|
233
|
-
' 2. Add your Supabase / R2 / SMTP values to .env.local (template in .env.example)',
|
|
234
|
-
),
|
|
235
|
-
);
|
|
236
|
-
console.log(chalk.cyan(' 3. npm run dev'));
|
|
239
|
+
await runCloudScaffold(projectDir, projectName);
|
|
237
240
|
}
|
|
238
241
|
} catch (error) {
|
|
239
242
|
console.error(
|
|
@@ -898,6 +901,65 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
898
901
|
);
|
|
899
902
|
}
|
|
900
903
|
|
|
904
|
+
// Local Self-Hosted Docker Mode: materialize the supabase migrations out of the installed
|
|
905
|
+
// @nextblock-cms/db package (the migration-runner container applies them on boot), then hand off
|
|
906
|
+
// to the project's own zero-dependency Docker setup script (prompts + .env + `docker compose up`).
|
|
907
|
+
async function runCloudScaffold(projectDir, projectName) {
|
|
908
|
+
const projectPath = resolve(projectDir);
|
|
909
|
+
|
|
910
|
+
// Materialize the Supabase assets (migrations + config) so `npm run db:migrate` works later,
|
|
911
|
+
// then hand off entirely to the browser First-Boot Setup Wizard for configuration. No
|
|
912
|
+
// credentials are collected in the terminal.
|
|
913
|
+
try {
|
|
914
|
+
await ensureSupabaseAssets(projectPath, { required: false });
|
|
915
|
+
} catch {
|
|
916
|
+
// Non-fatal: the wizard still works; db:migrate just needs these assets present.
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
console.log(
|
|
920
|
+
chalk.green(
|
|
921
|
+
`\nSuccess! Your NextBlock⢠CMS project "${projectName}" is scaffolded.\n`,
|
|
922
|
+
),
|
|
923
|
+
);
|
|
924
|
+
console.log(chalk.cyan('Next steps:'));
|
|
925
|
+
console.log(chalk.cyan(` 1. cd ${projectName}`));
|
|
926
|
+
console.log(chalk.cyan(' 2. npm run dev'));
|
|
927
|
+
console.log(
|
|
928
|
+
` 3. Open ${chalk.cyan('/setup')} ${chalk.gray('in your browser (the URL npm run dev prints, e.g. http://localhost:3000/setup)')}`,
|
|
929
|
+
);
|
|
930
|
+
console.log(
|
|
931
|
+
chalk.gray(' Connect Supabase, configure storage / email, and create your administrator.'),
|
|
932
|
+
);
|
|
933
|
+
console.log('');
|
|
934
|
+
console.log(chalk.gray(' Self-hosted Docker instead? npm run docker:setup'));
|
|
935
|
+
console.log(chalk.gray(' One-click cloud deploy: see docs/12-VERCEL-DEPLOYMENT.md'));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function runDockerSetup(projectDir, projectName) {
|
|
939
|
+
const projectPath = resolve(projectDir);
|
|
940
|
+
process.chdir(projectPath);
|
|
941
|
+
|
|
942
|
+
clack.intro('š³ NextBlock⢠CMS ā Local Self-Hosted Docker setup');
|
|
943
|
+
|
|
944
|
+
await ensureSupabaseAssets(projectPath, { required: true });
|
|
945
|
+
|
|
946
|
+
const setupScript = resolve(projectPath, 'scripts', 'docker-setup.mjs');
|
|
947
|
+
if (!(await fs.pathExists(setupScript))) {
|
|
948
|
+
clack.note(
|
|
949
|
+
'scripts/docker-setup.mjs is missing from the template. Run `npm run sync:create-nextblock` and try again.',
|
|
950
|
+
'Docker setup unavailable',
|
|
951
|
+
);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// The script drives docker compose interactively; inherit stdio so its prompts work.
|
|
956
|
+
await runCommand('node', ['scripts/docker-setup.mjs'], { cwd: projectPath });
|
|
957
|
+
|
|
958
|
+
clack.outro(
|
|
959
|
+
`š Your NextBlock⢠project ${projectName ? `"${projectName}" ` : ''}is running in Docker.\nApp: http://localhost:3000 (first sign-up becomes ADMIN)`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
901
963
|
async function configureHostedSupabaseAuth(
|
|
902
964
|
projectDir,
|
|
903
965
|
{ projectId, siteUrl, accessToken, smtpValues },
|
|
@@ -1924,7 +1986,11 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1924
1986
|
'/**',
|
|
1925
1987
|
" * @type {import('next').NextConfig}",
|
|
1926
1988
|
' **/',
|
|
1989
|
+
// Self-hosted Docker builds emit a standalone server (`node server.js`); gated on
|
|
1990
|
+
// DOCKER_BUILD so a normal `next build` / Vercel deploy is unaffected.
|
|
1991
|
+
"const isDockerStandalone = process.env.DOCKER_BUILD === 'true';",
|
|
1927
1992
|
'const nextConfig = {',
|
|
1993
|
+
" ...(isDockerStandalone ? { output: 'standalone' } : {}),",
|
|
1928
1994
|
' outputFileTracingRoot: path.join(__dirname),',
|
|
1929
1995
|
' env: {',
|
|
1930
1996
|
' 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,56 @@
|
|
|
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
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY}
|
|
21
|
+
SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
|
|
22
|
+
API_EXTERNAL_URL=http://localhost:8000
|
|
23
|
+
SITE_URL=http://localhost:3000
|
|
24
|
+
POSTGRES_DB=postgres
|
|
25
|
+
|
|
26
|
+
# --- App ------------------------------------------------------------------------------------
|
|
27
|
+
NEXT_PUBLIC_URL=http://localhost:3000
|
|
28
|
+
NEXT_PUBLIC_IS_SANDBOX=true
|
|
29
|
+
CORTEX_AI_ENCRYPTION_KEY=
|
|
30
|
+
|
|
31
|
+
# --- MinIO object storage (S3-compatible) ----------------------------------------------------
|
|
32
|
+
MINIO_ROOT_USER=nextblock
|
|
33
|
+
MINIO_ROOT_PASSWORD=replace-me-generated
|
|
34
|
+
STORAGE_BUCKET=nextblock
|
|
35
|
+
R2_ACCOUNT_ID=minio
|
|
36
|
+
R2_REGION=us-east-1
|
|
37
|
+
R2_S3_ENDPOINT=http://minio:9000
|
|
38
|
+
# 127.0.0.1 (not localhost) so the browser never sends the app's localhost cookies to MinIO,
|
|
39
|
+
# which would otherwise 400 (MetadataTooLarge) once the Supabase auth cookies grow.
|
|
40
|
+
R2_S3_PUBLIC_ENDPOINT=http://127.0.0.1:9000
|
|
41
|
+
R2_FORCE_PATH_STYLE=true
|
|
42
|
+
NEXT_PUBLIC_R2_BASE_URL=http://127.0.0.1:9000/nextblock
|
|
43
|
+
NEXT_PUBLIC_R2_PUBLIC_URL=http://127.0.0.1:9000/nextblock
|
|
44
|
+
|
|
45
|
+
# --- Cloudflare Turnstile (optional). Press Enter at setup to skip and use the dev sandbox. ---
|
|
46
|
+
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
|
47
|
+
TURNSTILE_SECRET_KEY=
|
|
48
|
+
|
|
49
|
+
# --- SMTP (optional). Blank => GoTrue auto-confirms sign-ups (loopback mode). -----------------
|
|
50
|
+
GOTRUE_MAILER_AUTOCONFIRM=true
|
|
51
|
+
SMTP_HOST=
|
|
52
|
+
SMTP_PORT=
|
|
53
|
+
SMTP_USER=
|
|
54
|
+
SMTP_PASS=
|
|
55
|
+
SMTP_FROM_EMAIL=
|
|
56
|
+
SMTP_FROM_NAME=
|
|
@@ -0,0 +1,85 @@
|
|
|
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 socat \
|
|
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
|
+
# ipv4first so localhost resolves to the socat IPv4 listener; the larger header limit absorbs
|
|
61
|
+
# Supabase auth cookies (on localhost they aren't port-scoped, so they pile onto every request).
|
|
62
|
+
NODE_OPTIONS="--dns-result-order=ipv4first --max-http-header-size=65536"
|
|
63
|
+
|
|
64
|
+
# A single-app project traces to its own root, so server.js sits at the top of .next/standalone.
|
|
65
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
66
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
67
|
+
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
|
68
|
+
|
|
69
|
+
# Loopback proxy: the browser-facing localhost URLs (Supabase :8000, MinIO :9000) are inlined into
|
|
70
|
+
# the build, but server-side code + next/image run INSIDE this container. Forward those ports to the
|
|
71
|
+
# compose services so SSR + image optimization work, and so the Supabase auth cookie key (derived
|
|
72
|
+
# from the URL host) matches between browser and server. Override targets with LOOPBACK_PROXIES.
|
|
73
|
+
RUN printf '%s\n' \
|
|
74
|
+
'#!/bin/sh' \
|
|
75
|
+
'set -e' \
|
|
76
|
+
': "${LOOPBACK_PROXIES:=8000:kong:8000 9000:minio:9000}"' \
|
|
77
|
+
'for p in $LOOPBACK_PROXIES; do' \
|
|
78
|
+
' socat "TCP4-LISTEN:${p%%:*},fork,reuseaddr,bind=127.0.0.1" "TCP:${p#*:}" 2>/dev/null &' \
|
|
79
|
+
'done' \
|
|
80
|
+
'exec node "$(cat .server-entry 2>/dev/null || echo server.js)"' \
|
|
81
|
+
> /app/docker-entrypoint.sh && chmod +x /app/docker-entrypoint.sh
|
|
82
|
+
|
|
83
|
+
USER nextjs
|
|
84
|
+
EXPOSE 3000
|
|
85
|
+
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
|
@@ -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
|