This guide covers setting up Supabase on your local server for FieldFleet.
- Docker & Docker Compose installed
- Domain name pointing to your server (e.g.,
api.example.com) - Ports 80/443 available (or configure reverse proxy)
# On your server
cd /opt
git clone --depth 1 https://github.com/supabase/supabase
cd supabase/dockercp .env.example .envEdit .env with secure values:
############
# Secrets - GENERATE NEW VALUES FOR PRODUCTION
############
# Generate with: openssl rand -base64 32
POSTGRES_PASSWORD=your-super-secret-postgres-password
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters
ANON_KEY=generate-from-jwt-secret
SERVICE_ROLE_KEY=generate-from-jwt-secret
############
# Database
############
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432
############
# API
############
SITE_URL=https://app.example.com
API_EXTERNAL_URL=https://api.example.com
############
# Auth
############
GOTRUE_SITE_URL=https://app.example.com
GOTRUE_URI_ALLOW_LIST=https://app.example.com,http://localhost:*
# Email (using Resend)
GOTRUE_SMTP_HOST=smtp.resend.com
GOTRUE_SMTP_PORT=465
GOTRUE_SMTP_USER=resend
GOTRUE_SMTP_PASS=your-resend-api-key
GOTRUE_SMTP_ADMIN_EMAIL=noreply@example.com
# OAuth (optional)
GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=your-google-client-id
GOTRUE_EXTERNAL_GOOGLE_SECRET=your-google-client-secret
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=https://api.example.com/auth/v1/callback
############
# Storage
############
STORAGE_BACKEND=file
FILE_SIZE_LIMIT=209715200 # 200MB
############
# Studio (Admin Dashboard)
############
STUDIO_DEFAULT_ORGANIZATION=FieldFleet
STUDIO_DEFAULT_PROJECT=FieldFleet# Generate ANON_KEY and SERVICE_ROLE_KEY
# Use https://supabase.com/docs/guides/self-hosting#api-keys
# Or use this script:
npm install -g @supabase/cli
supabase gen keys --jwt-secret "your-jwt-secret"Install Caddy:
apt install -y caddyCreate /etc/caddy/Caddyfile:
api.example.com {
reverse_proxy localhost:8000
}
studio.example.com {
reverse_proxy localhost:3000
}Reload Caddy:
systemctl reload caddycd /opt/supabase/docker
docker compose up -dCheck status:
docker compose ps
docker compose logs -fThis repository uses the Supabase CLI with Podman or Docker for local
development. The public checkout is configured as Supabase project
fieldfleet_public on ports 55321-55329. Those nonstandard ports avoid
accidentally connecting to another local Supabase stack that uses the default
54321-54329 range.
Before applying migrations on a workstation that already runs Supabase, inspect the active containers and migration history:
supabase status
docker ps --filter name=supabase_
docker exec supabase_db_fieldfleet_public psql -U postgres -d postgres \
-c "select count(*) from supabase_migrations.schema_migrations;"If the database has migration versions that are not present in this checkout,
or if it contains data you need, do not run supabase db reset, supabase migration repair, or supabase db pull. Use a separate project id/port range
or stop the unrelated stack intentionally before continuing.
On macOS with Podman, a reboot can leave the local Supabase containers stopped until the Podman VM is started again.
cd /path/to/fieldfleet
podman machine start
supabase startcurl -I http://127.0.0.1:55321
curl -I http://127.0.0.1:55323
docker ps --filter name=supabase_Expected ports:
- API gateway:
55321 - Studio:
55323 - Postgres:
55322
The API gateway root can return 404 Not Found to a curl -I request and
still be healthy; the important signal is that Kong responds on 55321.
Studio normally redirects to /project/default.
Most people expect a self-hostable app to be explorable immediately, but a
committed admin/admin account is unsafe because it tends to survive into real
deployments. This repo uses an explicit local-only helper instead:
scripts/create_local_demo_user.shDefault local credentials:
Email: demo@fieldfleet.local
Password: local-demo-pass
The helper reads the local anon and service-role keys from supabase status -o env, refuses non-local SUPABASE_URL values, and creates an email-confirmed
Supabase Auth user through the local Auth admin API. The first app login then
creates the public users row, workspace, and owner membership through the
normal bootstrap code path.
You can override the disposable credentials for one run:
DEMO_EMAIL=demo+$(date +%s)@fieldfleet.local \
DEMO_PASSWORD='replace-with-a-local-password' \
scripts/create_local_demo_user.shFor headless browser checks, prefer the production web build over Flutter's
debug web-server target:
flutter build web --no-pub \
--dart-define=SUPABASE_URL=http://127.0.0.1:55321 \
--dart-define=SUPABASE_ANON_KEY=your-local-supabase-anon-key
python3 -m http.server 5001 --bind 127.0.0.1 --directory build/webFlutter web renders most UI to canvas, so enable semantics before using accessibility locators:
document.querySelector('flt-semantics-placeholder')?.click();The login screen exposes Flutter-created text inputs only after they receive
focus. In Playwright, click the field first, then target the generated input
such as input[aria-label="you@example.com"] or input[type="password"].
The demo account should land on the authenticated dashboard and create one
local user, workspace, and membership through the normal bootstrap flow.
During public-checkout validation, a clean isolated replay exposed several classes of migration drift that should stay fixed:
- Local project id and ports must not overlap another Supabase stack. This repo
uses
fieldfleet_publicand55321-55329. - Public migrations must not assume private-only tables such as
public.notes. Guard legacy-table policy or DDL changes withto_regclass(...)checks. - Migration filenames must have unique timestamp prefixes; duplicate versions
fail when Supabase records
schema_migrations. - Index and function-hardening migrations should tolerate optional/private schema objects by checking for the column or function before altering it.
- Data migrations must target columns present in the public schema. Supplier
contact phone/email belongs in
vendor_contacts, not nonexistent vendor company phone/email columns.
If you see a vfkit app or process after startup, leave it running while you want local Supabase available. vfkit is the Podman VM backend, so closing it stops the container runtime and the Supabase stack with it.
# Copy migration files to server
scp -r supabase/migrations/* user@server:/opt/supabase/docker/volumes/db/
# Run migrations
docker compose exec db psql -U postgres -d postgres -f /var/lib/postgresql/data/001_initial_schema.sql
docker compose exec db psql -U postgres -d postgres -f /var/lib/postgresql/data/002_row_level_security.sql
docker compose exec db psql -U postgres -d postgres -f /var/lib/postgresql/data/003_storage_buckets.sqlOr use Supabase CLI:
supabase db push --db-url postgres://postgres:password@localhost:5432/postgresFor large file storage, mount a dedicated disk:
# Create storage directory
mkdir -p /mnt/storage/supabase
# Edit docker-compose.yml to use external volume
# Under storage service:
volumes:
- /mnt/storage/supabase:/var/lib/storageCreate /opt/supabase/backup.sh:
#!/bin/bash
BACKUP_DIR=/mnt/backups/supabase
DATE=$(date +%Y%m%d_%H%M%S)
# Database backup
docker compose exec -T db pg_dump -U postgres postgres | gzip > $BACKUP_DIR/db_$DATE.sql.gz
# Storage backup (optional - large)
# tar -czf $BACKUP_DIR/storage_$DATE.tar.gz /mnt/storage/supabase
# Keep last 7 days
find $BACKUP_DIR -name "*.gz" -mtime +7 -deleteAdd to crontab:
crontab -e
# Add: 0 2 * * * /opt/supabase/backup.sh- API Health:
curl https://api.example.com/rest/v1/ - Auth Health:
curl https://api.example.com/auth/v1/health - Studio: Open
https://studio.example.com
- Changed all default passwords
- Generated unique JWT secrets
- HTTPS configured (Caddy handles this)
- Firewall configured (only 80/443 open)
- Regular backups scheduled
- Studio behind authentication or VPN
Update your Flutter app to use the self-hosted instance:
// lib/config/supabase_config.dart
class SupabaseConfig {
static const String url = 'https://api.example.com';
static const String anonKey = 'your-anon-key';
}View logs:
# All services
docker compose logs -f
# Specific service
docker compose logs -f db
docker compose logs -f auth
docker compose logs -f rest
docker compose logs -f storagecd /opt/supabase/docker
git pull
docker compose pull
docker compose up -ddocker compose exec db psql -U postgres -c "SELECT 1"docker compose logs auth
# Check GOTRUE_* env vars# Check permissions
ls -la /mnt/storage/supabase
# Should be owned by container user (usually 1000:1000)
chown -R 1000:1000 /mnt/storage/supabaseEdit PostgreSQL config for your server specs:
# docker-compose.yml under db service
command: postgres -c shared_buffers=256MB -c max_connections=200