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
|
||||
|
||||
# Use app-only when Neta connects to an external Supabase-compatible backend.
|
||||
NETA_INSTALL_MODE=app-only
|
||||
|
||||
# Public URL where users open Neta.
|
||||
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.
|
||||
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.
|
||||
# Do not expose this to browsers. It is not required by the web container.
|
||||
# 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
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.full.example
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -50,13 +50,14 @@ Neta is engineered using modern, high-performance web technologies:
|
||||
|
||||
## 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
|
||||
- Docker and Docker Compose
|
||||
- A Supabase project or self-hosted Supabase backend
|
||||
- Supabase API URL, anon key, and service role key
|
||||
- A direct Postgres `DATABASE_URL` if you want the installer to apply migrations automatically
|
||||
- For app-only mode: Supabase API URL, anon key, service role key, and a direct Postgres `DATABASE_URL` for migrations
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
If you prefer to set up Neta manually:
|
||||
|
||||
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`.
|
||||
3. Fill every required value in `.env`.
|
||||
4. Apply database migrations:
|
||||
2. Navigate to the directory.
|
||||
3. For full-stack mode, generate a `.env` file and run:
|
||||
|
||||
```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
|
||||
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.
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
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_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: ${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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
```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.
|
||||
|
||||
@@ -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}"
|
||||
TARGET_DIR="${NETA_TARGET_DIR:-neta-os}"
|
||||
INSTALL_MODE="${NETA_INSTALL_MODE:-}"
|
||||
|
||||
info() {
|
||||
printf "\n%s\n" "$1"
|
||||
@@ -89,17 +90,135 @@ prompt_secret_required() {
|
||||
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() {
|
||||
cat > .env <<EOF
|
||||
NETA_INSTALL_MODE=$INSTALL_MODE
|
||||
NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
||||
NETA_PORT=$NETA_PORT
|
||||
NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
info "Neta self-host installer"
|
||||
|
||||
@@ -115,36 +234,42 @@ main() {
|
||||
git clone "$REPO_URL" "$TARGET_DIR"
|
||||
cd "$TARGET_DIR"
|
||||
|
||||
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"
|
||||
choose_install_mode
|
||||
|
||||
if [ "$INSTALL_MODE" = "full-stack" ]; then
|
||||
configure_full_stack
|
||||
else
|
||||
configure_app_only
|
||||
fi
|
||||
|
||||
write_env_file
|
||||
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
|
||||
if [ "$apply_migrations" = "y" ] || [ "$apply_migrations" = "Y" ]; then
|
||||
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
|
||||
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
|
||||
|
||||
local compose
|
||||
compose="$(compose_cmd)"
|
||||
|
||||
info "Validating Docker Compose configuration"
|
||||
$compose config >/dev/null
|
||||
|
||||
info "Building and starting Neta"
|
||||
$compose up -d --build
|
||||
run_compose "$compose"
|
||||
|
||||
info "Neta is starting"
|
||||
echo "Open: $NEXT_PUBLIC_SITE_URL"
|
||||
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 "$@"
|
||||
|
||||
@@ -5,9 +5,11 @@ export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
const supabaseUrl =
|
||||
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
supabaseUrl,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
|
||||
@@ -3,9 +3,11 @@ import { cookies } from 'next/headers'
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies()
|
||||
const supabaseUrl =
|
||||
process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
supabaseUrl,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"selfhost:env": "node scripts/generate-full-stack-env.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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
|
||||
echo "DATABASE_URL is required." >&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
|
||||
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() {
|
||||
local file_path="$1"
|
||||
local absolute_path="$ROOT_DIR/$file_path"
|
||||
file_path="$1"
|
||||
absolute_path="$ROOT_DIR/$file_path"
|
||||
|
||||
if [ ! -f "$absolute_path" ]; then
|
||||
echo "Missing SQL file: $file_path" >&2
|
||||
@@ -45,8 +33,19 @@ run_sql_file() {
|
||||
fi
|
||||
}
|
||||
|
||||
for sql_file in "${SQL_FILES[@]}"; do
|
||||
while IFS= read -r sql_file; do
|
||||
[ -n "$sql_file" ] || continue
|
||||
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."
|
||||
|
||||
@@ -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