refactor: enhance installation process with support for full-stack and app-only modes, update environment configurations, and improve database migration handling
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
# Neta self-host environment
|
# Neta self-host environment
|
||||||
|
|
||||||
|
# Use app-only when Neta connects to an external Supabase-compatible backend.
|
||||||
|
NETA_INSTALL_MODE=app-only
|
||||||
|
|
||||||
# Public URL where users open Neta.
|
# Public URL where users open Neta.
|
||||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
|
|
||||||
@@ -18,6 +21,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
|||||||
# Keep this secret. It is only used server-side.
|
# Keep this secret. It is only used server-side.
|
||||||
SUPABASE_SERVICE_ROLE_KEY=
|
SUPABASE_SERVICE_ROLE_KEY=
|
||||||
|
|
||||||
|
# Optional internal Supabase URL for Docker networks.
|
||||||
|
# In bundled mode this is set to http://neta-supabase-proxy:8000 by docker-compose.full.yml.
|
||||||
|
# SUPABASE_INTERNAL_URL=
|
||||||
|
|
||||||
# Optional: direct Postgres connection string used only by bash ./scripts/apply-migrations.sh.
|
# Optional: direct Postgres connection string used only by bash ./scripts/apply-migrations.sh.
|
||||||
# Do not expose this to browsers. It is not required by the web container.
|
# Do not expose this to browsers. It is not required by the web container.
|
||||||
# DATABASE_URL=postgresql://postgres:password@host:5432/postgres
|
# DATABASE_URL=postgresql://postgres:password@host:5432/postgres
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Neta full-stack self-host environment
|
||||||
|
|
||||||
|
NETA_INSTALL_MODE=full-stack
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
|
NETA_PORT=3000
|
||||||
|
|
||||||
|
# Public Supabase API URL reachable from the browser.
|
||||||
|
# For local Docker installs this is usually http://localhost:8000.
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
|
||||||
|
SUPABASE_API_PORT=8000
|
||||||
|
|
||||||
|
# Generated by install.sh in full-stack mode.
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=
|
||||||
|
JWT_SECRET=
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# Exposes bundled Postgres on the host for backups/manual access.
|
||||||
|
POSTGRES_PORT=54322
|
||||||
|
|
||||||
|
# Optional auth/mail settings.
|
||||||
|
JWT_EXPIRY=3600
|
||||||
|
SMTP_ADMIN_EMAIL=admin@neta.local
|
||||||
@@ -5,6 +5,7 @@ dist
|
|||||||
build
|
build
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.full.example
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|||||||
@@ -50,13 +50,14 @@ Neta is engineered using modern, high-performance web technologies:
|
|||||||
|
|
||||||
## Installation and Deployment
|
## Installation and Deployment
|
||||||
|
|
||||||
Neta is designed for self-hosting. The current Docker Compose file runs the Neta web application and connects it to a Supabase-compatible backend. A bundled Supabase Compose profile is planned for the full-stack self-host mode.
|
Neta is designed for self-hosting. It supports two Docker deployment modes:
|
||||||
|
|
||||||
|
- **Full-stack:** Neta + bundled Postgres/Auth/PostgREST/Storage.
|
||||||
|
- **App-only:** Neta connected to an existing Supabase-compatible backend.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Docker and Docker Compose
|
- Docker and Docker Compose
|
||||||
- A Supabase project or self-hosted Supabase backend
|
- For app-only mode: Supabase API URL, anon key, service role key, and a direct Postgres `DATABASE_URL` for migrations
|
||||||
- Supabase API URL, anon key, and service role key
|
|
||||||
- A direct Postgres `DATABASE_URL` if you want the installer to apply migrations automatically
|
|
||||||
|
|
||||||
### 1-Click Installation (Recommended)
|
### 1-Click Installation (Recommended)
|
||||||
|
|
||||||
@@ -66,22 +67,31 @@ You can install Neta using the interactive setup script:
|
|||||||
curl -sL https://raw.githubusercontent.com/poyrazavsever/neta/main/install.sh | bash
|
curl -sL https://raw.githubusercontent.com/poyrazavsever/neta/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer asks for the required Supabase values, writes a `.env` file, optionally applies database migrations, validates Docker Compose configuration, and starts the application.
|
The installer asks for the deployment mode, writes a `.env` file, validates Docker Compose configuration, and starts the application. In full-stack mode it generates Supabase JWT secrets and applies Neta migrations automatically through the `neta-migrations` service.
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
If you prefer to set up Neta manually:
|
If you prefer to set up Neta manually:
|
||||||
|
|
||||||
1. Clone the repository: `git clone https://github.com/poyrazavsever/neta.git`
|
1. Clone the repository: `git clone https://github.com/poyrazavsever/neta.git`
|
||||||
2. Navigate to the directory and copy the `.env.example` file to `.env`.
|
2. Navigate to the directory.
|
||||||
3. Fill every required value in `.env`.
|
3. For full-stack mode, generate a `.env` file and run:
|
||||||
4. Apply database migrations:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL='postgresql://postgres:password@host:5432/postgres' bash ./scripts/apply-migrations.sh
|
node scripts/generate-full-stack-env.mjs > .env
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Build and start the Docker container:
|
```bash
|
||||||
|
docker compose -f docker-compose.full.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. For app-only mode, copy `.env.example` to `.env`, fill every required value, and apply database migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Start the app-only Docker container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
@@ -89,6 +99,8 @@ docker compose up -d --build
|
|||||||
|
|
||||||
Docker Compose intentionally fails fast when required Supabase environment values are missing.
|
Docker Compose intentionally fails fast when required Supabase environment values are missing.
|
||||||
|
|
||||||
|
Coolify and Dokploy users should use `docker-compose.full.yml` for the no-external-service setup, or `docker-compose.yml` when connecting to an existing Supabase backend. See `docs/deployment/self-hosting.md` for the deployment checklist.
|
||||||
|
|
||||||
### First Administrator Account
|
### First Administrator Account
|
||||||
To ensure data security, Neta is locked to a single administrator. Upon launching the application for the first time, navigate to the `/register` route to create the initial admin account. Once this account is created, public registration is permanently disabled.
|
To ensure data security, Neta is locked to a single administrator. Upon launching the application for the first time, navigate to the `/register` route to create the initial admin account. Once this account is created, public registration is permanently disabled.
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl =
|
||||||
|
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
|
||||||
if (!supabaseUrl || !serviceRoleKey) {
|
if (!supabaseUrl || !serviceRoleKey) {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<SQL
|
||||||
|
create role anon nologin;
|
||||||
|
create role authenticated nologin;
|
||||||
|
create role service_role nologin bypassrls;
|
||||||
|
create role authenticator noinherit login password '$POSTGRES_PASSWORD';
|
||||||
|
create role supabase_auth_admin noinherit login password '$POSTGRES_PASSWORD';
|
||||||
|
create role supabase_storage_admin noinherit login password '$POSTGRES_PASSWORD';
|
||||||
|
|
||||||
|
grant anon, authenticated, service_role to authenticator;
|
||||||
|
grant all privileges on database postgres to supabase_auth_admin;
|
||||||
|
grant all privileges on database postgres to supabase_storage_admin;
|
||||||
|
|
||||||
|
create schema if not exists auth authorization supabase_auth_admin;
|
||||||
|
create schema if not exists storage authorization supabase_storage_admin;
|
||||||
|
|
||||||
|
grant usage on schema public to anon, authenticated, service_role;
|
||||||
|
grant usage on schema auth to supabase_auth_admin;
|
||||||
|
grant usage on schema storage to anon, authenticated, service_role, supabase_storage_admin;
|
||||||
|
|
||||||
|
create extension if not exists "uuid-ossp";
|
||||||
|
create extension if not exists pgcrypto;
|
||||||
|
create extension if not exists vector;
|
||||||
|
|
||||||
|
create or replace function auth.uid()
|
||||||
|
returns uuid
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as \$\$
|
||||||
|
select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid;
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
create or replace function auth.role()
|
||||||
|
returns text
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as \$\$
|
||||||
|
select nullif(current_setting('request.jwt.claim.role', true), '')::text;
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
create or replace function auth.email()
|
||||||
|
returns text
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as \$\$
|
||||||
|
select nullif(current_setting('request.jwt.claim.email', true), '')::text;
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
alter default privileges in schema public grant select, insert, update, delete on tables to anon, authenticated, service_role;
|
||||||
|
alter default privileges in schema public grant usage, select on sequences to anon, authenticated, service_role;
|
||||||
|
SQL
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
server {
|
||||||
|
listen 8000;
|
||||||
|
server_name _;
|
||||||
|
client_max_body_size 50m;
|
||||||
|
|
||||||
|
location /auth/v1/ {
|
||||||
|
proxy_pass http://neta-auth:9999/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /rest/v1/ {
|
||||||
|
proxy_pass http://neta-rest:3000/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /storage/v1/ {
|
||||||
|
proxy_pass http://neta-storage:5000/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
return 200 "ok\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
services:
|
||||||
|
neta-web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
|
||||||
|
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
||||||
|
container_name: neta-web
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
neta-migrations:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
neta-supabase-proxy:
|
||||||
|
condition: service_started
|
||||||
|
ports:
|
||||||
|
- "${NETA_PORT:-3000}:3000"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
|
||||||
|
SUPABASE_INTERNAL_URL: http://neta-supabase-proxy:8000
|
||||||
|
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
||||||
|
|
||||||
|
neta-supabase-proxy:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: neta-supabase-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
neta-auth:
|
||||||
|
condition: service_started
|
||||||
|
neta-rest:
|
||||||
|
condition: service_started
|
||||||
|
neta-storage:
|
||||||
|
condition: service_started
|
||||||
|
ports:
|
||||||
|
- "${SUPABASE_API_PORT:-8000}:8000"
|
||||||
|
volumes:
|
||||||
|
- ./deploy/supabase/nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro
|
||||||
|
|
||||||
|
neta-auth:
|
||||||
|
image: supabase/gotrue:v2.177.0
|
||||||
|
container_name: neta-auth
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
neta-db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
GOTRUE_API_HOST: 0.0.0.0
|
||||||
|
GOTRUE_API_PORT: 9999
|
||||||
|
API_EXTERNAL_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}/auth/v1
|
||||||
|
GOTRUE_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
||||||
|
GOTRUE_URI_ALLOW_LIST: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
||||||
|
GOTRUE_DB_DRIVER: postgres
|
||||||
|
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
|
||||||
|
GOTRUE_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
|
||||||
|
GOTRUE_JWT_EXP: ${JWT_EXPIRY:-3600}
|
||||||
|
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||||
|
GOTRUE_DISABLE_SIGNUP: "false"
|
||||||
|
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
|
||||||
|
GOTRUE_MAILER_AUTOCONFIRM: "true"
|
||||||
|
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-admin@neta.local}
|
||||||
|
GOTRUE_SMTP_HOST: ${SMTP_HOST:-}
|
||||||
|
GOTRUE_SMTP_PORT: ${SMTP_PORT:-587}
|
||||||
|
GOTRUE_SMTP_USER: ${SMTP_USER:-}
|
||||||
|
GOTRUE_SMTP_PASS: ${SMTP_PASS:-}
|
||||||
|
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Neta}
|
||||||
|
|
||||||
|
neta-rest:
|
||||||
|
image: postgrest/postgrest:v12.2.12
|
||||||
|
container_name: neta-rest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
neta-db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
|
||||||
|
PGRST_DB_SCHEMAS: public,storage
|
||||||
|
PGRST_DB_ANON_ROLE: anon
|
||||||
|
PGRST_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
|
||||||
|
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||||
|
|
||||||
|
neta-storage:
|
||||||
|
image: supabase/storage-api:v1.24.7
|
||||||
|
container_name: neta-storage
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
neta-db:
|
||||||
|
condition: service_healthy
|
||||||
|
neta-rest:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
|
||||||
|
SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
|
||||||
|
POSTGREST_URL: http://neta-rest:3000
|
||||||
|
PGRST_JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env}
|
||||||
|
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
|
||||||
|
FILE_SIZE_LIMIT: ${STORAGE_FILE_SIZE_LIMIT:-52428800}
|
||||||
|
STORAGE_BACKEND: file
|
||||||
|
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||||
|
TENANT_ID: stub
|
||||||
|
REGION: local
|
||||||
|
GLOBAL_S3_BUCKET: stub
|
||||||
|
ENABLE_IMAGE_TRANSFORMATION: "false"
|
||||||
|
volumes:
|
||||||
|
- neta-storage-data:/var/lib/storage
|
||||||
|
|
||||||
|
neta-migrations:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: neta-migrations
|
||||||
|
depends_on:
|
||||||
|
neta-db:
|
||||||
|
condition: service_healthy
|
||||||
|
neta-storage:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}@neta-db:5432/postgres
|
||||||
|
volumes:
|
||||||
|
- ./supabase:/app/supabase:ro
|
||||||
|
- ./scripts:/app/scripts:ro
|
||||||
|
working_dir: /app
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
tries=0
|
||||||
|
until psql "$$DATABASE_URL" -v ON_ERROR_STOP=1 -tAc "select case when to_regclass('auth.users') is not null and to_regclass('storage.buckets') is not null then 'ready' else 'waiting' end" | grep -q "ready"; do
|
||||||
|
tries=$$((tries + 1))
|
||||||
|
if [ "$$tries" -gt 60 ]; then
|
||||||
|
echo "Timed out waiting for Supabase Auth/Storage schemas."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Waiting for Supabase Auth/Storage schemas..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
sh ./scripts/apply-migrations.sh
|
||||||
|
|
||||||
|
neta-db:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
container_name: neta-db
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-54322}:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
|
||||||
|
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
volumes:
|
||||||
|
- neta-db-data:/var/lib/postgresql/data
|
||||||
|
- ./deploy/supabase/db/init.sh:/docker-entrypoint-initdb.d/00-neta-init.sh:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
neta-db-data:
|
||||||
|
neta-storage-data:
|
||||||
@@ -16,4 +16,5 @@ services:
|
|||||||
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
|
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:?Set NEXT_PUBLIC_SUPABASE_ANON_KEY in .env}
|
||||||
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
|
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:?Set SUPABASE_SERVICE_ROLE_KEY in .env}
|
||||||
|
SUPABASE_INTERNAL_URL: ${SUPABASE_INTERNAL_URL:-${NEXT_PUBLIC_SUPABASE_URL:?Set NEXT_PUBLIC_SUPABASE_URL in .env}}
|
||||||
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ They must also be documented and registered in this file, but they should only b
|
|||||||
Use the migration helper from the repository root:
|
Use the migration helper from the repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL='postgresql://postgres:password@host:5432/postgres' bash ./scripts/apply-migrations.sh
|
DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The helper applies `0001` through `0009` in the order listed above. It uses local `psql` when available, otherwise it runs `psql` through Docker.
|
The helper applies `0001` through `0009` in the order listed above. It uses local `psql` when available, otherwise it runs `psql` through Docker.
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Self-Hosting Deployment
|
||||||
|
|
||||||
|
Neta has two Docker modes.
|
||||||
|
|
||||||
|
## Full-Stack Mode
|
||||||
|
|
||||||
|
Use this when the server should run Neta and the bundled Supabase-compatible stack.
|
||||||
|
|
||||||
|
Compose file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose.full.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Services:
|
||||||
|
|
||||||
|
- `neta-web`: Next.js app
|
||||||
|
- `neta-db`: Postgres with pgvector
|
||||||
|
- `neta-auth`: Supabase Auth
|
||||||
|
- `neta-rest`: PostgREST
|
||||||
|
- `neta-storage`: Supabase Storage with local disk backend
|
||||||
|
- `neta-supabase-proxy`: single public API entrypoint
|
||||||
|
- `neta-migrations`: one-shot Neta database migration runner
|
||||||
|
|
||||||
|
Generate production secrets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/generate-full-stack-env.mjs > .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Then adjust these values before starting:
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_SITE_URL`: public Neta URL
|
||||||
|
- `NEXT_PUBLIC_SUPABASE_URL`: public bundled Supabase API URL
|
||||||
|
- `NETA_PORT`: host port for Neta when deploying directly with Docker
|
||||||
|
- `SUPABASE_API_PORT`: host port for the bundled Supabase API
|
||||||
|
- `POSTGRES_PORT`: host port for backup/manual DB access
|
||||||
|
|
||||||
|
Start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.full.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## App-Only Mode
|
||||||
|
|
||||||
|
Use this when Neta connects to an existing Supabase or Supabase-compatible backend.
|
||||||
|
|
||||||
|
Compose file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Required env values:
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_SITE_URL`
|
||||||
|
- `NEXT_PUBLIC_SUPABASE_URL`
|
||||||
|
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||||
|
- `SUPABASE_SERVICE_ROLE_KEY`
|
||||||
|
- `DATABASE_URL` only when applying migrations manually
|
||||||
|
|
||||||
|
Apply migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coolify
|
||||||
|
|
||||||
|
For a no-external-service install, choose Docker Compose deployment and set the compose file to `docker-compose.full.yml`.
|
||||||
|
|
||||||
|
Set the generated full-stack env values in Coolify's environment variables. Route the app domain to `neta-web:3000`. If you expose Supabase through a second domain, route it to `neta-supabase-proxy:8000` and set `NEXT_PUBLIC_SUPABASE_URL` to that public URL.
|
||||||
|
|
||||||
|
## Dokploy
|
||||||
|
|
||||||
|
Create a Compose app from this repository. Use `docker-compose.full.yml` for full-stack deployments and paste the generated env values into the environment panel.
|
||||||
|
|
||||||
|
Route the Neta domain to `neta-web` port `3000`. Route the bundled Supabase API domain, if used, to `neta-supabase-proxy` port `8000`.
|
||||||
|
|
||||||
|
## First Admin
|
||||||
|
|
||||||
|
Open `/register` after the stack starts. The first registered user becomes the admin, and public registration is locked after that.
|
||||||
+137
-12
@@ -6,6 +6,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
REPO_URL="${NETA_REPO_URL:-https://github.com/poyrazavsever/neta.git}"
|
REPO_URL="${NETA_REPO_URL:-https://github.com/poyrazavsever/neta.git}"
|
||||||
TARGET_DIR="${NETA_TARGET_DIR:-neta-os}"
|
TARGET_DIR="${NETA_TARGET_DIR:-neta-os}"
|
||||||
|
INSTALL_MODE="${NETA_INSTALL_MODE:-}"
|
||||||
|
|
||||||
info() {
|
info() {
|
||||||
printf "\n%s\n" "$1"
|
printf "\n%s\n" "$1"
|
||||||
@@ -89,17 +90,135 @@ prompt_secret_required() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
choose_install_mode() {
|
||||||
|
if [ -n "$INSTALL_MODE" ]; then
|
||||||
|
case "$INSTALL_MODE" in
|
||||||
|
full|full-stack|bundled)
|
||||||
|
INSTALL_MODE="full-stack"
|
||||||
|
;;
|
||||||
|
app|app-only|external)
|
||||||
|
INSTALL_MODE="app-only"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Invalid NETA_INSTALL_MODE. Use full-stack or app-only."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
export INSTALL_MODE
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Choose install mode:"
|
||||||
|
echo " 1) full-stack Neta + bundled Supabase/Postgres/Auth/Storage"
|
||||||
|
echo " 2) app-only Neta app connected to an existing Supabase project"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -r -p "Install mode [full-stack]: " answer
|
||||||
|
case "${answer:-full-stack}" in
|
||||||
|
1|full|full-stack|bundled)
|
||||||
|
INSTALL_MODE="full-stack"
|
||||||
|
export INSTALL_MODE
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
2|app|app-only|external)
|
||||||
|
INSTALL_MODE="app-only"
|
||||||
|
export INSTALL_MODE
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Please choose full-stack or app-only."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
run_node_script() {
|
||||||
|
local script="$1"
|
||||||
|
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
node -e "$script"
|
||||||
|
else
|
||||||
|
docker run --rm \
|
||||||
|
-e ROLE="${ROLE:-}" \
|
||||||
|
-e JWT_SECRET="${JWT_SECRET:-}" \
|
||||||
|
node:22-alpine node -e "$script"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
random_secret() {
|
||||||
|
run_node_script "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_supabase_jwt() {
|
||||||
|
local role="$1"
|
||||||
|
local secret="$2"
|
||||||
|
ROLE="$role" JWT_SECRET="$secret" run_node_script "const crypto=require('crypto'); const b64=(v)=>Buffer.from(v).toString('base64url'); const header=b64(JSON.stringify({alg:'HS256',typ:'JWT'})); const payload=b64(JSON.stringify({iss:'supabase',ref:'neta',role:process.env.ROLE,iat:1700000000,exp:4102444800})); const unsigned=header+'.'+payload; const sig=crypto.createHmac('sha256', process.env.JWT_SECRET).update(unsigned).digest('base64url'); console.log(unsigned+'.'+sig);"
|
||||||
|
}
|
||||||
|
|
||||||
write_env_file() {
|
write_env_file() {
|
||||||
cat > .env <<EOF
|
cat > .env <<EOF
|
||||||
|
NETA_INSTALL_MODE=$INSTALL_MODE
|
||||||
NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
||||||
NETA_PORT=$NETA_PORT
|
NETA_PORT=$NETA_PORT
|
||||||
NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
|
NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||||
SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY
|
SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
if [ "$INSTALL_MODE" = "full-stack" ]; then
|
||||||
|
cat >> .env <<EOF
|
||||||
|
SUPABASE_API_PORT=$SUPABASE_API_PORT
|
||||||
|
POSTGRES_PORT=$POSTGRES_PORT
|
||||||
|
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||||
|
JWT_SECRET=$JWT_SECRET
|
||||||
|
JWT_EXPIRY=${JWT_EXPIRY:-3600}
|
||||||
|
SMTP_ADMIN_EMAIL=${SMTP_ADMIN_EMAIL:-admin@neta.local}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
chmod 600 .env || true
|
chmod 600 .env || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure_app_only() {
|
||||||
|
prompt_optional NEXT_PUBLIC_SITE_URL "Public Neta URL" "http://localhost:3000"
|
||||||
|
prompt_optional NETA_PORT "Host port for Neta" "3000"
|
||||||
|
prompt_required NEXT_PUBLIC_SUPABASE_URL "Supabase API URL"
|
||||||
|
prompt_secret_required NEXT_PUBLIC_SUPABASE_ANON_KEY "Supabase anon key"
|
||||||
|
prompt_secret_required SUPABASE_SERVICE_ROLE_KEY "Supabase service role key"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_full_stack() {
|
||||||
|
prompt_optional NEXT_PUBLIC_SITE_URL "Public Neta URL" "http://localhost:3000"
|
||||||
|
prompt_optional NETA_PORT "Host port for Neta" "3000"
|
||||||
|
prompt_optional NEXT_PUBLIC_SUPABASE_URL "Public Supabase API URL" "http://localhost:8000"
|
||||||
|
prompt_optional SUPABASE_API_PORT "Host port for bundled Supabase API" "8000"
|
||||||
|
prompt_optional POSTGRES_PORT "Host port for bundled Postgres" "54322"
|
||||||
|
|
||||||
|
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-$(random_secret)}"
|
||||||
|
JWT_SECRET="${JWT_SECRET:-$(random_secret)}"
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY:-$(generate_supabase_jwt anon "$JWT_SECRET")}"
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY="${SUPABASE_SERVICE_ROLE_KEY:-$(generate_supabase_jwt service_role "$JWT_SECRET")}"
|
||||||
|
|
||||||
|
export POSTGRES_PASSWORD JWT_SECRET NEXT_PUBLIC_SUPABASE_ANON_KEY SUPABASE_SERVICE_ROLE_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_compose() {
|
||||||
|
local compose="$1"
|
||||||
|
|
||||||
|
if [ "$INSTALL_MODE" = "full-stack" ]; then
|
||||||
|
info "Validating full-stack Docker Compose configuration"
|
||||||
|
$compose -f docker-compose.full.yml config >/dev/null
|
||||||
|
|
||||||
|
info "Building and starting bundled Neta stack"
|
||||||
|
$compose -f docker-compose.full.yml up -d --build
|
||||||
|
else
|
||||||
|
info "Validating Docker Compose configuration"
|
||||||
|
$compose config >/dev/null
|
||||||
|
|
||||||
|
info "Building and starting Neta"
|
||||||
|
$compose up -d --build
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
info "Neta self-host installer"
|
info "Neta self-host installer"
|
||||||
|
|
||||||
@@ -115,36 +234,42 @@ main() {
|
|||||||
git clone "$REPO_URL" "$TARGET_DIR"
|
git clone "$REPO_URL" "$TARGET_DIR"
|
||||||
cd "$TARGET_DIR"
|
cd "$TARGET_DIR"
|
||||||
|
|
||||||
prompt_optional NEXT_PUBLIC_SITE_URL "Public Neta URL" "http://localhost:3000"
|
choose_install_mode
|
||||||
prompt_optional NETA_PORT "Host port for Neta" "3000"
|
|
||||||
prompt_required NEXT_PUBLIC_SUPABASE_URL "Supabase API URL"
|
if [ "$INSTALL_MODE" = "full-stack" ]; then
|
||||||
prompt_secret_required NEXT_PUBLIC_SUPABASE_ANON_KEY "Supabase anon key"
|
configure_full_stack
|
||||||
prompt_secret_required SUPABASE_SERVICE_ROLE_KEY "Supabase service role key"
|
else
|
||||||
|
configure_app_only
|
||||||
|
fi
|
||||||
|
|
||||||
write_env_file
|
write_env_file
|
||||||
info "Wrote .env"
|
info "Wrote .env"
|
||||||
|
|
||||||
|
if [ "$INSTALL_MODE" = "app-only" ]; then
|
||||||
read -r -p "Apply Neta database migrations now? Requires a direct Postgres DATABASE_URL. [y/N]: " apply_migrations
|
read -r -p "Apply Neta database migrations now? Requires a direct Postgres DATABASE_URL. [y/N]: " apply_migrations
|
||||||
if [ "$apply_migrations" = "y" ] || [ "$apply_migrations" = "Y" ]; then
|
if [ "$apply_migrations" = "y" ] || [ "$apply_migrations" = "Y" ]; then
|
||||||
prompt_secret_required DATABASE_URL "Postgres DATABASE_URL"
|
prompt_secret_required DATABASE_URL "Postgres DATABASE_URL"
|
||||||
DATABASE_URL="$DATABASE_URL" bash ./scripts/apply-migrations.sh
|
DATABASE_URL="$DATABASE_URL" sh ./scripts/apply-migrations.sh
|
||||||
else
|
else
|
||||||
echo "Skipping migrations. Run them later with:"
|
echo "Skipping migrations. Run them later with:"
|
||||||
echo " DATABASE_URL='postgresql://...' bash ./scripts/apply-migrations.sh"
|
echo " DATABASE_URL='postgresql://...' sh ./scripts/apply-migrations.sh"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Bundled mode applies migrations automatically through the neta-migrations service."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local compose
|
local compose
|
||||||
compose="$(compose_cmd)"
|
compose="$(compose_cmd)"
|
||||||
|
|
||||||
info "Validating Docker Compose configuration"
|
run_compose "$compose"
|
||||||
$compose config >/dev/null
|
|
||||||
|
|
||||||
info "Building and starting Neta"
|
|
||||||
$compose up -d --build
|
|
||||||
|
|
||||||
info "Neta is starting"
|
info "Neta is starting"
|
||||||
echo "Open: $NEXT_PUBLIC_SITE_URL"
|
echo "Open: $NEXT_PUBLIC_SITE_URL"
|
||||||
echo "Create the first admin account at: $NEXT_PUBLIC_SITE_URL/register"
|
echo "Create the first admin account at: $NEXT_PUBLIC_SITE_URL/register"
|
||||||
|
if [ "$INSTALL_MODE" = "full-stack" ]; then
|
||||||
|
echo "Bundled Supabase API: $NEXT_PUBLIC_SUPABASE_URL"
|
||||||
|
echo "Bundled Postgres host port: $POSTGRES_PORT"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ export async function updateSession(request: NextRequest) {
|
|||||||
let supabaseResponse = NextResponse.next({
|
let supabaseResponse = NextResponse.next({
|
||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
|
const supabaseUrl =
|
||||||
|
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||||
|
|
||||||
const supabase = createServerClient(
|
const supabase = createServerClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
supabaseUrl,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
{
|
{
|
||||||
cookies: {
|
cookies: {
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { cookies } from 'next/headers'
|
|||||||
|
|
||||||
export async function createClient() {
|
export async function createClient() {
|
||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
|
const supabaseUrl =
|
||||||
|
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||||
|
|
||||||
return createServerClient(
|
return createServerClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
supabaseUrl,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
{
|
{
|
||||||
cookies: {
|
cookies: {
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"selfhost:env": "node scripts/generate-full-stack-env.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/google": "^3.0.80",
|
"@ai-sdk/google": "^3.0.80",
|
||||||
|
|||||||
+19
-20
@@ -1,31 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
set -euo pipefail
|
set -eu
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
|
||||||
if [ -z "${DATABASE_URL:-}" ]; then
|
if [ -z "${DATABASE_URL:-}" ]; then
|
||||||
echo "DATABASE_URL is required." >&2
|
echo "DATABASE_URL is required." >&2
|
||||||
echo "Example:" >&2
|
echo "Example:" >&2
|
||||||
echo " DATABASE_URL='postgresql://postgres:password@host:5432/postgres' bash ./scripts/apply-migrations.sh" >&2
|
echo " DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SQL_FILES=(
|
|
||||||
"supabase/schema.sql"
|
|
||||||
"supabase/migrations/0002_add_freelancer_os_core_tables.sql"
|
|
||||||
"supabase/migrations/0003_add_project_planning_assets.sql"
|
|
||||||
"supabase/migrations/0004_add_business_os_tables.sql"
|
|
||||||
"supabase/migrations/0005_add_advanced_crm_tables.sql"
|
|
||||||
"supabase/migrations/0006_add_pgvector_and_embeddings.sql"
|
|
||||||
"supabase/migrations/0007_add_client_portal_tables.sql"
|
|
||||||
"supabase/migrations/0008_add_project_progress_and_quota.sql"
|
|
||||||
"supabase/migrations/0009_lock_registration_after_first_admin.sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
run_sql_file() {
|
run_sql_file() {
|
||||||
local file_path="$1"
|
file_path="$1"
|
||||||
local absolute_path="$ROOT_DIR/$file_path"
|
absolute_path="$ROOT_DIR/$file_path"
|
||||||
|
|
||||||
if [ ! -f "$absolute_path" ]; then
|
if [ ! -f "$absolute_path" ]; then
|
||||||
echo "Missing SQL file: $file_path" >&2
|
echo "Missing SQL file: $file_path" >&2
|
||||||
@@ -45,8 +33,19 @@ run_sql_file() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
for sql_file in "${SQL_FILES[@]}"; do
|
while IFS= read -r sql_file; do
|
||||||
|
[ -n "$sql_file" ] || continue
|
||||||
run_sql_file "$sql_file"
|
run_sql_file "$sql_file"
|
||||||
done
|
done <<'SQL_FILES'
|
||||||
|
supabase/schema.sql
|
||||||
|
supabase/migrations/0002_add_freelancer_os_core_tables.sql
|
||||||
|
supabase/migrations/0003_add_project_planning_assets.sql
|
||||||
|
supabase/migrations/0004_add_business_os_tables.sql
|
||||||
|
supabase/migrations/0005_add_advanced_crm_tables.sql
|
||||||
|
supabase/migrations/0006_add_pgvector_and_embeddings.sql
|
||||||
|
supabase/migrations/0007_add_client_portal_tables.sql
|
||||||
|
supabase/migrations/0008_add_project_progress_and_quota.sql
|
||||||
|
supabase/migrations/0009_lock_registration_after_first_admin.sql
|
||||||
|
SQL_FILES
|
||||||
|
|
||||||
echo "All Neta migrations were applied."
|
echo "All Neta migrations were applied."
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
const jwtSecret = process.env.JWT_SECRET || randomSecret();
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
NETA_INSTALL_MODE: "full-stack",
|
||||||
|
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000",
|
||||||
|
NETA_PORT: process.env.NETA_PORT || "3000",
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL:
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL || "http://localhost:8000",
|
||||||
|
SUPABASE_API_PORT: process.env.SUPABASE_API_PORT || "8000",
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY:
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || generateSupabaseJwt("anon", jwtSecret),
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY:
|
||||||
|
process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
||||||
|
generateSupabaseJwt("service_role", jwtSecret),
|
||||||
|
JWT_SECRET: jwtSecret,
|
||||||
|
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD || randomSecret(),
|
||||||
|
POSTGRES_PORT: process.env.POSTGRES_PORT || "54322",
|
||||||
|
JWT_EXPIRY: process.env.JWT_EXPIRY || "3600",
|
||||||
|
SMTP_ADMIN_EMAIL: process.env.SMTP_ADMIN_EMAIL || "admin@neta.local",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
console.log(`${key}=${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomSecret() {
|
||||||
|
return crypto.randomBytes(32).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSupabaseJwt(role, secret) {
|
||||||
|
const header = base64UrlJson({ alg: "HS256", typ: "JWT" });
|
||||||
|
const payload = base64UrlJson({
|
||||||
|
iss: "supabase",
|
||||||
|
ref: "neta",
|
||||||
|
role,
|
||||||
|
iat: 1700000000,
|
||||||
|
exp: 4102444800,
|
||||||
|
});
|
||||||
|
const unsigned = `${header}.${payload}`;
|
||||||
|
const signature = crypto
|
||||||
|
.createHmac("sha256", secret)
|
||||||
|
.update(unsigned)
|
||||||
|
.digest("base64url");
|
||||||
|
|
||||||
|
return `${unsigned}.${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64UrlJson(value) {
|
||||||
|
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user