create-nextblock 0.8.11 → 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.
@@ -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,215 @@
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
+ SUPABASE_ANON_KEY: ${ANON_KEY}
102
+ SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
103
+ volumes:
104
+ - ./docker/kong/kong.yml:/home/kong/temp.yml:ro
105
+ entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
106
+ ports:
107
+ - '${KONG_HTTP_PORT:-8000}:8000'
108
+
109
+ minio:
110
+ image: ${MINIO_IMAGE:-minio/minio:latest}
111
+ restart: unless-stopped
112
+ command: server /data --console-address ":9001"
113
+ environment:
114
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER}
115
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
116
+ volumes:
117
+ - nextblock_media:/data
118
+ ports:
119
+ - '${MINIO_S3_PORT:-9000}:9000'
120
+ - '${MINIO_CONSOLE_PORT:-9001}:9001'
121
+
122
+ minio-init:
123
+ image: ${MINIO_MC_IMAGE:-minio/mc:latest}
124
+ restart: 'no'
125
+ depends_on:
126
+ minio:
127
+ condition: service_started
128
+ environment:
129
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER}
130
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
131
+ STORAGE_BUCKET: ${STORAGE_BUCKET:-nextblock}
132
+ entrypoint: >
133
+ /bin/sh -c "
134
+ until mc alias set local http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD; do echo 'waiting for minio...'; sleep 2; done &&
135
+ mc mb --ignore-existing local/$$STORAGE_BUCKET &&
136
+ mc anonymous set download local/$$STORAGE_BUCKET &&
137
+ echo 'minio bucket ready'
138
+ "
139
+
140
+ migrate:
141
+ image: ${MIGRATE_IMAGE:-postgres:15-alpine}
142
+ restart: 'no'
143
+ depends_on:
144
+ db:
145
+ condition: service_healthy
146
+ auth:
147
+ condition: service_healthy
148
+ environment:
149
+ POSTGRES_HOST: db
150
+ POSTGRES_PORT: 5432
151
+ POSTGRES_DB: ${POSTGRES_DB:-postgres}
152
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
153
+ volumes:
154
+ - ./supabase/migrations:/migrations:ro
155
+ - ./docker/migrate/run-migrations.sh:/run-migrations.sh:ro
156
+ entrypoint: ['sh', '/run-migrations.sh']
157
+
158
+ nextblock-cms:
159
+ build:
160
+ context: .
161
+ dockerfile: Dockerfile
162
+ args:
163
+ NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
164
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
165
+ NEXT_PUBLIC_URL: ${NEXT_PUBLIC_URL:-http://localhost:3000}
166
+ NEXT_PUBLIC_R2_PUBLIC_URL: ${NEXT_PUBLIC_R2_PUBLIC_URL}
167
+ NEXT_PUBLIC_R2_BASE_URL: ${NEXT_PUBLIC_R2_BASE_URL}
168
+ NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-}
169
+ NEXT_PUBLIC_IS_SANDBOX: ${NEXT_PUBLIC_IS_SANDBOX:-true}
170
+ restart: unless-stopped
171
+ depends_on:
172
+ migrate:
173
+ condition: service_completed_successfully
174
+ kong:
175
+ condition: service_started
176
+ minio-init:
177
+ condition: service_completed_successfully
178
+ environment:
179
+ NODE_ENV: production
180
+ PORT: 3000
181
+ HOSTNAME: 0.0.0.0
182
+ NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
183
+ SUPABASE_INTERNAL_URL: ${SUPABASE_INTERNAL_URL:-http://kong:8000}
184
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
185
+ SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
186
+ NEXT_PUBLIC_URL: ${NEXT_PUBLIC_URL:-http://localhost:3000}
187
+ NEXT_PUBLIC_IS_SANDBOX: ${NEXT_PUBLIC_IS_SANDBOX:-true}
188
+ CRON_SECRET: ${CRON_SECRET}
189
+ DRAFT_MODE_SECRET: ${DRAFT_MODE_SECRET}
190
+ REVALIDATE_SECRET_TOKEN: ${REVALIDATE_SECRET_TOKEN}
191
+ CORTEX_AI_ENCRYPTION_KEY: ${CORTEX_AI_ENCRYPTION_KEY:-}
192
+ R2_ACCOUNT_ID: ${R2_ACCOUNT_ID:-minio}
193
+ R2_BUCKET_NAME: ${STORAGE_BUCKET:-nextblock}
194
+ R2_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
195
+ R2_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
196
+ R2_REGION: ${R2_REGION:-us-east-1}
197
+ R2_S3_ENDPOINT: ${R2_S3_ENDPOINT:-http://minio:9000}
198
+ R2_S3_PUBLIC_ENDPOINT: ${R2_S3_PUBLIC_ENDPOINT:-http://localhost:9000}
199
+ R2_FORCE_PATH_STYLE: 'true'
200
+ NEXT_PUBLIC_R2_PUBLIC_URL: ${NEXT_PUBLIC_R2_PUBLIC_URL}
201
+ NEXT_PUBLIC_R2_BASE_URL: ${NEXT_PUBLIC_R2_BASE_URL}
202
+ SMTP_HOST: ${SMTP_HOST:-}
203
+ SMTP_PORT: ${SMTP_PORT:-}
204
+ SMTP_USER: ${SMTP_USER:-}
205
+ SMTP_PASS: ${SMTP_PASS:-}
206
+ SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
207
+ SMTP_FROM_NAME: ${SMTP_FROM_NAME:-}
208
+ TURNSTILE_SECRET_KEY: ${TURNSTILE_SECRET_KEY:-}
209
+ NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-}
210
+ ports:
211
+ - '${APP_PORT:-3000}:3000'
212
+
213
+ volumes:
214
+ nextblock_db_store:
215
+ nextblock_media:
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  vi.mock('server-only', () => ({}));
4
4
 
5
5
  const mocks = vi.hoisted(() => ({
6
- getS3ClientMock: vi.fn(),
6
+ getS3PresignClientMock: vi.fn(),
7
7
  getSignedUrlMock: vi.fn(),
8
8
  getUserMock: vi.fn(),
9
9
  profileSingleMock: vi.fn(),
@@ -28,7 +28,7 @@ vi.mock('@nextblock-cms/db/server', () => ({
28
28
  }));
29
29
 
30
30
  vi.mock('@nextblock-cms/utils/server', () => ({
31
- getS3Client: mocks.getS3ClientMock,
31
+ getS3PresignClient: mocks.getS3PresignClientMock,
32
32
  }));
33
33
 
34
34
  vi.mock('@aws-sdk/client-s3', () => ({
@@ -82,7 +82,7 @@ describe('custom block R2 presigned upload flow', () => {
82
82
  );
83
83
 
84
84
  expect(response.status).toBe(401);
85
- expect(mocks.getS3ClientMock).not.toHaveBeenCalled();
85
+ expect(mocks.getS3PresignClientMock).not.toHaveBeenCalled();
86
86
  });
87
87
 
88
88
  it('rejects oversized files for authorized writers', async () => {
@@ -103,13 +103,13 @@ describe('custom block R2 presigned upload flow', () => {
103
103
 
104
104
  expect(response.status).toBe(400);
105
105
  expect(payload.error).toContain('10 MB');
106
- expect(mocks.getS3ClientMock).not.toHaveBeenCalled();
106
+ expect(mocks.getS3PresignClientMock).not.toHaveBeenCalled();
107
107
  });
108
108
 
109
109
  it('returns a direct PUT upload space for authorized writers', async () => {
110
110
  mocks.getUserMock.mockResolvedValueOnce({ data: { user: { id: 'user-1' } }, error: null });
111
111
  mocks.profileSingleMock.mockResolvedValueOnce({ data: { role: 'ADMIN' }, error: null });
112
- mocks.getS3ClientMock.mockResolvedValueOnce({ send: vi.fn() });
112
+ mocks.getS3PresignClientMock.mockResolvedValueOnce({ send: vi.fn() });
113
113
  mocks.getSignedUrlMock.mockResolvedValueOnce('https://r2.example.test/presigned-put');
114
114
 
115
115
  const response = await POST(
@@ -2,7 +2,7 @@ import 'server-only';
2
2
 
3
3
  import { PutObjectCommand } from '@aws-sdk/client-s3';
4
4
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5
- import { getS3Client } from '@nextblock-cms/utils/server';
5
+ import { getS3PresignClient } from '@nextblock-cms/utils/server';
6
6
 
7
7
  import { resolveMediaUrl } from './media/resolveMediaUrl';
8
8
  import {
@@ -35,7 +35,7 @@ export async function createR2PresignedUpload(
35
35
  throw new R2PresignedUploadError('File uploads are not configured on this server.', 500);
36
36
  }
37
37
 
38
- const s3Client = await getS3Client();
38
+ const s3Client = await getS3PresignClient();
39
39
  if (!s3Client) {
40
40
  throw new R2PresignedUploadError('File uploads are not configured on this server.', 500);
41
41
  }
@@ -81,8 +81,17 @@ const securityHeaders = [
81
81
  },
82
82
  ];
83
83
 
84
+ // Self-hosted Docker images build a standalone server (`node apps/nextblock/server.js`) instead
85
+ // of `next start` + a full node_modules tree. Gated on DOCKER_BUILD so Vercel/cloud builds are
86
+ // completely untouched. outputFileTracingRoot is pinned to the monorepo root so the standalone
87
+ // output nests predictably under apps/nextblock (see the root Dockerfile runner stage).
88
+ const isDockerStandalone = process.env.DOCKER_BUILD === 'true';
89
+
84
90
  /** @type {import('next').NextConfig} */
85
91
  const nextConfig = {
92
+ ...(isDockerStandalone
93
+ ? { output: 'standalone', outputFileTracingRoot: path.join(__dirname, '../../') }
94
+ : {}),
86
95
  experimental: {
87
96
  optimizePackageImports: ['@nextblock-cms/ui', '@nextblock-cms/utils'],
88
97
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/template",
3
- "version": "0.8.11",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -8,7 +8,11 @@
8
8
  "start": "next start",
9
9
  "lint": "next lint",
10
10
  "deploy:supabase": "node tools/deploy-supabase.js",
11
- "configure:supabase-auth": "node tools/configure-supabase-auth.js"
11
+ "configure:supabase-auth": "node tools/configure-supabase-auth.js",
12
+ "docker:setup": "node scripts/docker-setup.mjs",
13
+ "docker:up": "docker compose up -d --build",
14
+ "docker:down": "docker compose down",
15
+ "docker:logs": "docker compose logs -f nextblock-cms"
12
16
  },
13
17
  "dependencies": {
14
18
  "@ai-sdk/openai-compatible": "^2.0.42",