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:
Poyraz Avsever
2026-06-13 21:23:28 +03:00
parent 17ff60faea
commit 0f89d9f627
17 changed files with 620 additions and 51 deletions
+7
View File
@@ -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
+23
View File
@@ -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
+1
View File
@@ -5,6 +5,7 @@ dist
build
.env*
!.env.example
!.env.full.example
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+22 -10
View File
@@ -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.
+2 -1
View File
@@ -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) {
+53
View File
@@ -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;
}
}
+162
View File
@@ -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:
+1
View File
@@ -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}
+1 -1
View File
@@ -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.
+89
View File
@@ -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
View File
@@ -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 "$@"
+3 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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."
+54
View File
@@ -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");
}