Compare commits

...

10 Commits

Author SHA1 Message Date
5ea19bba26 Add database schema with RLS policies 2025-12-12 11:01:11 +01:00
cb3a98e5e0 Add Mailjet credentials 2025-12-12 10:50:21 +01:00
597a43b589 Update SMTP to Mailjet 2025-12-12 10:45:32 +01:00
e170cefb5d Update SMTP to Mailjet 2025-12-12 10:45:27 +01:00
b3fe2e4dac Add bootstrap script for VPS deployment 2025-12-12 10:42:53 +01:00
c75edbfb8a Add deployment script 2025-12-12 10:42:39 +01:00
7c6dbef443 Add edge function placeholder 2025-12-12 10:42:35 +01:00
ee80f9a10f Add Kong API gateway config 2025-12-12 10:42:31 +01:00
67979d3cd1 Add Supabase .env.example 2025-12-12 10:42:24 +01:00
c9113c9293 Add Supabase docker-compose.yml 2025-12-12 10:42:06 +01:00
7 changed files with 1244 additions and 0 deletions

84
supabase/.env.example Normal file
View File

@@ -0,0 +1,84 @@
############################################################
# SECRETS - MUST BE CHANGED FOR PRODUCTION
# Generate with: openssl rand -hex 32
############################################################
# Database password (32+ chars)
POSTGRES_PASSWORD=your-super-secret-postgres-password-change-me
# JWT Secret (32+ chars hex)
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters
# API Keys - Generate at: https://supabase.com/docs/guides/self-hosting#api-keys
# Or use: node -e "const jwt=require('jsonwebtoken');console.log(jwt.sign({role:'anon',iss:'supabase',iat:Math.floor(Date.now()/1000),exp:Math.floor(Date.now()/1000)+315360000},'YOUR_JWT_SECRET'))"
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
# Dashboard credentials - CHANGE THESE
DASHBOARD_USERNAME=supabase_admin
DASHBOARD_PASSWORD=your-secure-dashboard-password
# Encryption keys (32 chars each)
SECRET_KEY_BASE=your-64-character-secret-key-base-for-realtime
VAULT_ENC_KEY=your-32-character-vault-encryption
PG_META_CRYPTO_KEY=your-32-character-pg-meta-crypto
############################################################
# DATABASE
############################################################
POSTGRES_DB=postgres
POSTGRES_PORT=5432
PGRST_DB_SCHEMAS=public,storage,graphql_public
############################################################
# URLS
############################################################
SITE_URL=https://mylder.io
API_EXTERNAL_URL=https://supabase.mylder.io
SUPABASE_PUBLIC_URL=https://supabase.mylder.io
ADDITIONAL_REDIRECT_URLS=
############################################################
# AUTH
############################################################
JWT_EXPIRY=3600
DISABLE_SIGNUP=false
ENABLE_EMAIL_SIGNUP=true
ENABLE_EMAIL_AUTOCONFIRM=false
ENABLE_ANONYMOUS_SIGN_INS=false
ENABLE_PHONE_SIGNUP=false
ENABLE_PHONE_AUTOCONFIRM=false
############################################################
# SMTP (for magic link emails)
# Using Mailjet - get API keys from https://app.mailjet.com/account/apikeys
############################################################
SMTP_ADMIN_EMAIL=admin@mylder.io
SMTP_HOST=in-v3.mailjet.com
SMTP_PORT=587
SMTP_USER=your-mailjet-api-key
SMTP_PASS=your-mailjet-secret-key
SMTP_SENDER_NAME=Mylder
# Email paths
MAILER_URLPATHS_INVITE=/auth/v1/verify
MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
MAILER_URLPATHS_RECOVERY=/auth/v1/verify
MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify
############################################################
# STUDIO
############################################################
STUDIO_DEFAULT_ORGANIZATION=Mylder
STUDIO_DEFAULT_PROJECT=Main
IMGPROXY_ENABLE_WEBP_DETECTION=true
############################################################
# FUNCTIONS
############################################################
FUNCTIONS_VERIFY_JWT=true
############################################################
# ANALYTICS
############################################################
LOGFLARE_API_KEY=your-logflare-api-key

23
supabase/bootstrap.sh Normal file
View File

@@ -0,0 +1,23 @@
#\!/bin/bash
# Supabase Bootstrap for Mylder VPS
set -e
echo "=== Supabase Bootstrap ==="
cd /srv
# Clone infrastructure repo
if [ -d "supabase" ]; then
echo "Removing existing supabase directory..."
rm -rf supabase
fi
# Clone from Gitea (public access via HTTPS)
git clone https://gitea.mylder.io/admin/infrastructure.git /tmp/infrastructure
mv /tmp/infrastructure/supabase /srv/supabase
rm -rf /tmp/infrastructure
cd /srv/supabase
chmod +x deploy.sh
echo "=== Files deployed. Run: cd /srv/supabase && ./deploy.sh ==="

192
supabase/deploy.sh Normal file
View File

@@ -0,0 +1,192 @@
#!/bin/bash
# Supabase Self-Hosted Deployment Script for Mylder VPS
# Run this script on the VPS as root
set -e
echo "=== Supabase Self-Hosted Deployment ==="
echo ""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
SUPABASE_DIR="/srv/supabase"
COMPOSE_FILE="$SUPABASE_DIR/docker-compose.yml"
ENV_FILE="$SUPABASE_DIR/.env"
# Step 1: Create directory structure
echo -e "${YELLOW}Step 1: Creating directory structure...${NC}"
mkdir -p $SUPABASE_DIR/volumes/{api,db/data,db/init,storage,functions/main,logs}
cd $SUPABASE_DIR
# Step 2: Generate secrets
echo -e "${YELLOW}Step 2: Generating secrets...${NC}"
POSTGRES_PASSWORD=$(openssl rand -hex 24)
JWT_SECRET=$(openssl rand -hex 32)
SECRET_KEY_BASE=$(openssl rand -base64 48 | tr -d '\n')
VAULT_ENC_KEY=$(openssl rand -hex 16)
PG_META_CRYPTO_KEY=$(openssl rand -hex 16)
DASHBOARD_PASSWORD=$(openssl rand -base64 16 | tr -d '\n')
LOGFLARE_API_KEY=$(openssl rand -hex 16)
echo -e "${GREEN}Generated secrets (save these!):${NC}"
echo "POSTGRES_PASSWORD: $POSTGRES_PASSWORD"
echo "JWT_SECRET: $JWT_SECRET"
echo "DASHBOARD_PASSWORD: $DASHBOARD_PASSWORD"
# Step 3: Generate JWT tokens
echo -e "${YELLOW}Step 3: Generating JWT tokens...${NC}"
# Install Node.js jwt-cli if not present
if ! command -v node &> /dev/null; then
echo "Node.js not found. Installing..."
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
fi
# Generate ANON and SERVICE_ROLE keys using Node.js
ANON_KEY=$(node -e "
const crypto = require('crypto');
const header = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT'})).toString('base64url');
const payload = Buffer.from(JSON.stringify({role:'anon',iss:'supabase',iat:Math.floor(Date.now()/1000),exp:Math.floor(Date.now()/1000)+315360000})).toString('base64url');
const signature = crypto.createHmac('sha256','$JWT_SECRET').update(header+'.'+payload).digest('base64url');
console.log(header+'.'+payload+'.'+signature);
")
SERVICE_ROLE_KEY=$(node -e "
const crypto = require('crypto');
const header = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT'})).toString('base64url');
const payload = Buffer.from(JSON.stringify({role:'service_role',iss:'supabase',iat:Math.floor(Date.now()/1000),exp:Math.floor(Date.now()/1000)+315360000})).toString('base64url');
const signature = crypto.createHmac('sha256','$JWT_SECRET').update(header+'.'+payload).digest('base64url');
console.log(header+'.'+payload+'.'+signature);
")
echo "ANON_KEY: $ANON_KEY"
echo "SERVICE_ROLE_KEY: $SERVICE_ROLE_KEY"
# Step 4: Create .env file
echo -e "${YELLOW}Step 4: Creating .env file...${NC}"
cat > $ENV_FILE << EOF
############################################################
# SECRETS
############################################################
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
JWT_SECRET=$JWT_SECRET
ANON_KEY=$ANON_KEY
SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=$DASHBOARD_PASSWORD
SECRET_KEY_BASE=$SECRET_KEY_BASE
VAULT_ENC_KEY=$VAULT_ENC_KEY
PG_META_CRYPTO_KEY=$PG_META_CRYPTO_KEY
############################################################
# DATABASE
############################################################
POSTGRES_DB=postgres
POSTGRES_PORT=5432
PGRST_DB_SCHEMAS=public,storage,graphql_public
############################################################
# URLS
############################################################
SITE_URL=https://mylder.io
API_EXTERNAL_URL=https://supabase.mylder.io
SUPABASE_PUBLIC_URL=https://supabase.mylder.io
ADDITIONAL_REDIRECT_URLS=
############################################################
# AUTH
############################################################
JWT_EXPIRY=3600
DISABLE_SIGNUP=false
ENABLE_EMAIL_SIGNUP=true
ENABLE_EMAIL_AUTOCONFIRM=false
ENABLE_ANONYMOUS_SIGN_INS=false
ENABLE_PHONE_SIGNUP=false
ENABLE_PHONE_AUTOCONFIRM=false
############################################################
# SMTP (configure with Mailjet API keys)
# Get keys from: https://app.mailjet.com/account/apikeys
############################################################
SMTP_ADMIN_EMAIL=admin@mylder.io
SMTP_HOST=in-v3.mailjet.com
SMTP_PORT=587
SMTP_USER=f42a859cc0e03f91af90849be4c981fc
SMTP_PASS=22fc7cbc55e4b515702b5264f4b1636e
SMTP_SENDER_NAME=Mylder
MAILER_URLPATHS_INVITE=/auth/v1/verify
MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
MAILER_URLPATHS_RECOVERY=/auth/v1/verify
MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify
############################################################
# STUDIO
############################################################
STUDIO_DEFAULT_ORGANIZATION=Mylder
STUDIO_DEFAULT_PROJECT=Main
IMGPROXY_ENABLE_WEBP_DETECTION=true
############################################################
# FUNCTIONS & ANALYTICS
############################################################
FUNCTIONS_VERIFY_JWT=true
LOGFLARE_API_KEY=$LOGFLARE_API_KEY
EOF
chmod 600 $ENV_FILE
echo -e "${GREEN}.env file created${NC}"
# Step 5: Check if docker-compose.yml exists
if [ ! -f "$COMPOSE_FILE" ]; then
echo -e "${RED}docker-compose.yml not found at $COMPOSE_FILE${NC}"
echo "Please copy the docker-compose.yml file to $SUPABASE_DIR"
exit 1
fi
# Step 6: Check if kong.yml exists
if [ ! -f "$SUPABASE_DIR/volumes/api/kong.yml" ]; then
echo -e "${RED}kong.yml not found${NC}"
echo "Please copy volumes/api/kong.yml to $SUPABASE_DIR/volumes/api/"
exit 1
fi
# Step 7: Pull images
echo -e "${YELLOW}Step 5: Pulling Docker images...${NC}"
docker compose pull
# Step 8: Start services
echo -e "${YELLOW}Step 6: Starting Supabase services...${NC}"
docker compose up -d
# Step 9: Wait for health checks
echo -e "${YELLOW}Step 7: Waiting for services to be healthy...${NC}"
sleep 30
# Step 10: Check status
echo -e "${YELLOW}Step 8: Checking service status...${NC}"
docker compose ps
echo ""
echo -e "${GREEN}=== Deployment Complete ===${NC}"
echo ""
echo "Access Supabase Studio at: https://supabase.mylder.io"
echo "Username: admin"
echo "Password: $DASHBOARD_PASSWORD"
echo ""
echo "API URL: https://supabase.mylder.io"
echo "ANON_KEY: $ANON_KEY"
echo "SERVICE_ROLE_KEY: $SERVICE_ROLE_KEY"
echo ""
echo -e "${YELLOW}IMPORTANT: Save these credentials securely!${NC}"
echo ""
echo "Next steps:"
echo "1. Add DNS A record for supabase.mylder.io pointing to this server"
echo "2. Configure SMTP (replace SMTP_USER/SMTP_PASS in .env with Mailjet API keys)"
echo "3. Access Studio and create your database schema"

368
supabase/docker-compose.yml Normal file
View File

@@ -0,0 +1,368 @@
version: "3.8"
services:
# ===========================================
# DATABASE
# ===========================================
supabase-db:
image: supabase/postgres:15.8.1.21
container_name: supabase-db
restart: unless-stopped
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
command:
- postgres
- -c
- config_file=/etc/postgresql/postgresql.conf
- -c
- log_min_messages=fatal
environment:
POSTGRES_HOST: /var/run/postgresql
PGPORT: ${POSTGRES_PORT:-5432}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
PGPASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATABASE: ${POSTGRES_DB:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-postgres}
JWT_SECRET: ${JWT_SECRET}
JWT_EXP: ${JWT_EXPIRY:-3600}
volumes:
- supabase_db_data:/var/lib/postgresql/data:Z
networks:
- supabase-internal
- dokploy-network
# ===========================================
# API GATEWAY (KONG)
# ===========================================
supabase-kong:
image: kong:2.8.1
container_name: supabase-kong
restart: unless-stopped
depends_on:
supabase-analytics:
condition: service_healthy
entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
volumes:
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro
networks:
- supabase-internal
- dokploy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.supabase-api.rule=Host(`supabase.mylder.io`)"
- "traefik.http.routers.supabase-api.entrypoints=websecure"
- "traefik.http.routers.supabase-api.tls.certResolver=letsencrypt"
- "traefik.http.services.supabase-api.loadbalancer.server.port=8000"
- "traefik.docker.network=dokploy-network"
# ===========================================
# AUTHENTICATION (GOTRUE)
# ===========================================
supabase-auth:
image: supabase/gotrue:v2.172.0
container_name: supabase-auth
restart: unless-stopped
depends_on:
supabase-db:
condition: service_healthy
supabase-analytics:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"]
interval: 5s
timeout: 5s
retries: 3
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@supabase-db:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
GOTRUE_SITE_URL: ${SITE_URL}
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP:-false}
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: ${JWT_EXPIRY:-3600}
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP:-true}
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_SIGN_INS:-false}
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM:-false}
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
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:-Mylder}
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE:-/auth/v1/verify}
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION:-/auth/v1/verify}
GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY:-/auth/v1/verify}
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE:-/auth/v1/verify}
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP:-false}
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM:-false}
networks:
- supabase-internal
# ===========================================
# REST API (POSTGREST)
# ===========================================
supabase-rest:
image: postgrest/postgrest:v12.2.0
container_name: supabase-rest
restart: unless-stopped
depends_on:
supabase-db:
condition: service_healthy
supabase-analytics:
condition: service_healthy
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@supabase-db:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS:-public,storage,graphql_public}
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY:-3600}
command: "postgrest"
networks:
- supabase-internal
# ===========================================
# REALTIME
# ===========================================
supabase-realtime:
image: supabase/realtime:v2.68.0
container_name: supabase-realtime
restart: unless-stopped
depends_on:
supabase-db:
condition: service_healthy
supabase-analytics:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sSfL", "--head", "-o", "/dev/null", "http://localhost:4000/api/health"]
interval: 10s
timeout: 5s
retries: 5
environment:
PORT: 4000
DB_HOST: supabase-db
DB_PORT: ${POSTGRES_PORT:-5432}
DB_USER: supabase_admin
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: ${POSTGRES_DB:-postgres}
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
DB_ENC_KEY: ${VAULT_ENC_KEY}
API_JWT_SECRET: ${JWT_SECRET}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
ERL_AFLAGS: -proto_dist inet_tcp
DNS_NODES: "''"
RLIMIT_NOFILE: "10000"
APP_NAME: realtime
SEED_SELF_HOST: "true"
RUN_JANITOR: "true"
command: >
bash -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
networks:
- supabase-internal
# ===========================================
# STORAGE
# ===========================================
supabase-storage:
image: supabase/storage-api:v1.17.0
container_name: supabase-storage
restart: unless-stopped
depends_on:
supabase-db:
condition: service_healthy
supabase-rest:
condition: service_started
supabase-imgproxy:
condition: service_started
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status"]
interval: 5s
timeout: 5s
retries: 3
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://supabase-rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@supabase-db:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
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: "true"
IMGPROXY_URL: http://supabase-imgproxy:5001
volumes:
- supabase_storage_data:/var/lib/storage:z
networks:
- supabase-internal
# ===========================================
# IMAGE PROXY
# ===========================================
supabase-imgproxy:
image: darthsim/imgproxy:v3.8.0
container_name: supabase-imgproxy
restart: unless-stopped
healthcheck:
test: ["CMD", "imgproxy", "health"]
interval: 5s
timeout: 5s
retries: 3
environment:
IMGPROXY_BIND: ":5001"
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION:-true}
volumes:
- supabase_storage_data:/var/lib/storage:z
networks:
- supabase-internal
# ===========================================
# META (PG-META)
# ===========================================
supabase-meta:
image: supabase/postgres-meta:v0.89.0
container_name: supabase-meta
restart: unless-stopped
depends_on:
supabase-db:
condition: service_healthy
supabase-analytics:
condition: service_healthy
environment:
PG_META_PORT: 8080
PG_META_HOST: 0.0.0.0
PG_META_DB_HOST: supabase-db
PG_META_DB_PORT: ${POSTGRES_PORT:-5432}
PG_META_DB_NAME: ${POSTGRES_DB:-postgres}
PG_META_DB_USER: supabase_admin
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
networks:
- supabase-internal
# ===========================================
# EDGE FUNCTIONS
# ===========================================
supabase-functions:
image: supabase/edge-runtime:v1.69.0
container_name: supabase-functions
restart: unless-stopped
depends_on:
supabase-analytics:
condition: service_healthy
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://supabase-kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@supabase-db:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
VERIFY_JWT: ${FUNCTIONS_VERIFY_JWT:-true}
volumes:
- ./volumes/functions:/home/deno/functions:Z
command:
- start
- --main-service
- /home/deno/functions/main
networks:
- supabase-internal
# ===========================================
# ANALYTICS (LOGFLARE)
# ===========================================
supabase-analytics:
image: supabase/logflare:1.12.0
container_name: supabase-analytics
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-sSfL", "--head", "-o", "/dev/null", "http://localhost:4000/health"]
interval: 5s
timeout: 5s
retries: 10
depends_on:
supabase-db:
condition: service_healthy
environment:
LOGFLARE_SINGLE_TENANT: "true"
LOGFLARE_SUPABASE_MODE: "true"
LOGFLARE_NODE_HOST: 127.0.0.1
DB_USERNAME: supabase_admin
DB_DATABASE: ${POSTGRES_DB:-postgres}
DB_HOSTNAME: supabase-db
DB_PORT: ${POSTGRES_PORT:-5432}
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_SCHEMA: _analytics
LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@supabase-db:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
POSTGRES_BACKEND_SCHEMA: _analytics
LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
networks:
- supabase-internal
# ===========================================
# STUDIO (DASHBOARD)
# ===========================================
supabase-studio:
image: supabase/studio:2024.12.02-sha-8fac0a1
container_name: supabase-studio
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/platform/health', (r) => process.exit(r.statusCode !== 200 ? 1 : 0)).on('error', () => process.exit(1))"]
interval: 5s
timeout: 5s
retries: 3
depends_on:
supabase-analytics:
condition: service_healthy
environment:
STUDIO_PG_META_URL: http://supabase-meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION:-Mylder}
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT:-Main}
SUPABASE_URL: http://supabase-kong:8000
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
AUTH_JWT_SECRET: ${JWT_SECRET}
LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
LOGFLARE_URL: http://supabase-analytics:4000
NEXT_PUBLIC_ENABLE_LOGS: "true"
NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
networks:
- supabase-internal
networks:
supabase-internal:
name: supabase-internal
driver: bridge
dokploy-network:
external: true
volumes:
supabase_db_data:
supabase_storage_data:

332
supabase/schema.sql Normal file
View File

@@ -0,0 +1,332 @@
-- Mylder Platform Database Schema
-- Run this in Supabase Studio SQL Editor after deployment
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================
-- PROFILES (extends auth.users)
-- ============================================
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
email TEXT NOT NULL,
full_name TEXT,
avatar_url TEXT,
role TEXT DEFAULT 'user' CHECK (role IN ('user', 'admin')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Auto-create profile on user signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name, avatar_url)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
-- ============================================
-- TEAMS
-- ============================================
CREATE TABLE IF NOT EXISTS public.teams (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name TEXT NOT NULL,
owner_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
stripe_customer_id TEXT,
plan TEXT DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- TEAM MEMBERS
-- ============================================
CREATE TABLE IF NOT EXISTS public.team_members (
team_id UUID REFERENCES public.teams(id) ON DELETE CASCADE,
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE,
role TEXT DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (team_id, user_id)
);
-- Auto-add owner as team member
CREATE OR REPLACE FUNCTION public.handle_new_team()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.team_members (team_id, user_id, role)
VALUES (NEW.id, NEW.owner_id, 'owner');
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
DROP TRIGGER IF EXISTS on_team_created ON public.teams;
CREATE TRIGGER on_team_created
AFTER INSERT ON public.teams
FOR EACH ROW EXECUTE FUNCTION public.handle_new_team();
-- ============================================
-- PROJECTS
-- ============================================
CREATE TABLE IF NOT EXISTS public.projects (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
team_id UUID REFERENCES public.teams(id) ON DELETE CASCADE NOT NULL,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT,
gitea_repo TEXT,
tech_stack TEXT[] DEFAULT '{}',
platform TEXT,
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'paused')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(team_id, slug)
);
-- ============================================
-- MESSAGES
-- ============================================
CREATE TABLE IF NOT EXISTS public.messages (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
project_id UUID REFERENCES public.projects(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- AGENT RUNS
-- ============================================
CREATE TABLE IF NOT EXISTS public.agent_runs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
message_id UUID REFERENCES public.messages(id) ON DELETE CASCADE NOT NULL,
project_id UUID REFERENCES public.projects(id) ON DELETE CASCADE NOT NULL,
command TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
result JSONB,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- ISSUES (synced from Gitea)
-- ============================================
CREATE TABLE IF NOT EXISTS public.issues (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
project_id UUID REFERENCES public.projects(id) ON DELETE CASCADE NOT NULL,
gitea_id INTEGER,
title TEXT NOT NULL,
body TEXT,
state TEXT DEFAULT 'open' CHECK (state IN ('open', 'closed')),
labels TEXT[] DEFAULT '{}',
assignee TEXT,
milestone TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(project_id, gitea_id)
);
-- ============================================
-- UPDATED_AT TRIGGER
-- ============================================
CREATE OR REPLACE FUNCTION public.update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply to all tables with updated_at
DROP TRIGGER IF EXISTS update_profiles_updated_at ON public.profiles;
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON public.profiles
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
DROP TRIGGER IF EXISTS update_teams_updated_at ON public.teams;
CREATE TRIGGER update_teams_updated_at
BEFORE UPDATE ON public.teams
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
DROP TRIGGER IF EXISTS update_projects_updated_at ON public.projects;
CREATE TRIGGER update_projects_updated_at
BEFORE UPDATE ON public.projects
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
DROP TRIGGER IF EXISTS update_issues_updated_at ON public.issues;
CREATE TRIGGER update_issues_updated_at
BEFORE UPDATE ON public.issues
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
-- ============================================
-- ROW LEVEL SECURITY (RLS)
-- ============================================
-- Enable RLS on all tables
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.teams ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.team_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.agent_runs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.issues ENABLE ROW LEVEL SECURITY;
-- PROFILES
CREATE POLICY "Users can view own profile" ON public.profiles
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update own profile" ON public.profiles
FOR UPDATE USING (auth.uid() = id);
-- TEAMS
CREATE POLICY "Team members can view their teams" ON public.teams
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.team_members
WHERE team_members.team_id = teams.id
AND team_members.user_id = auth.uid()
)
);
CREATE POLICY "Users can create teams" ON public.teams
FOR INSERT WITH CHECK (auth.uid() = owner_id);
CREATE POLICY "Team owners can update teams" ON public.teams
FOR UPDATE USING (auth.uid() = owner_id);
CREATE POLICY "Team owners can delete teams" ON public.teams
FOR DELETE USING (auth.uid() = owner_id);
-- TEAM MEMBERS
CREATE POLICY "Team members can view team membership" ON public.team_members
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.team_members tm
WHERE tm.team_id = team_members.team_id
AND tm.user_id = auth.uid()
)
);
-- PROJECTS
CREATE POLICY "Team members can view projects" ON public.projects
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.team_members
WHERE team_members.team_id = projects.team_id
AND team_members.user_id = auth.uid()
)
);
CREATE POLICY "Team members can create projects" ON public.projects
FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM public.team_members
WHERE team_members.team_id = projects.team_id
AND team_members.user_id = auth.uid()
)
);
CREATE POLICY "Team members can update projects" ON public.projects
FOR UPDATE USING (
EXISTS (
SELECT 1 FROM public.team_members
WHERE team_members.team_id = projects.team_id
AND team_members.user_id = auth.uid()
)
);
-- MESSAGES
CREATE POLICY "Team members can view messages" ON public.messages
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.projects p
JOIN public.team_members tm ON tm.team_id = p.team_id
WHERE p.id = messages.project_id
AND tm.user_id = auth.uid()
)
);
CREATE POLICY "Team members can create messages" ON public.messages
FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM public.projects p
JOIN public.team_members tm ON tm.team_id = p.team_id
WHERE p.id = messages.project_id
AND tm.user_id = auth.uid()
)
);
-- AGENT RUNS
CREATE POLICY "Team members can view agent runs" ON public.agent_runs
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.projects p
JOIN public.team_members tm ON tm.team_id = p.team_id
WHERE p.id = agent_runs.project_id
AND tm.user_id = auth.uid()
)
);
-- ISSUES
CREATE POLICY "Team members can view issues" ON public.issues
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.projects p
JOIN public.team_members tm ON tm.team_id = p.team_id
WHERE p.id = issues.project_id
AND tm.user_id = auth.uid()
)
);
CREATE POLICY "Team members can manage issues" ON public.issues
FOR ALL USING (
EXISTS (
SELECT 1 FROM public.projects p
JOIN public.team_members tm ON tm.team_id = p.team_id
WHERE p.id = issues.project_id
AND tm.user_id = auth.uid()
)
);
-- ============================================
-- REALTIME SUBSCRIPTIONS
-- ============================================
-- Enable realtime for specific tables
ALTER PUBLICATION supabase_realtime ADD TABLE public.messages;
ALTER PUBLICATION supabase_realtime ADD TABLE public.agent_runs;
ALTER PUBLICATION supabase_realtime ADD TABLE public.issues;
-- ============================================
-- INDEXES
-- ============================================
CREATE INDEX IF NOT EXISTS idx_team_members_user ON public.team_members(user_id);
CREATE INDEX IF NOT EXISTS idx_projects_team ON public.projects(team_id);
CREATE INDEX IF NOT EXISTS idx_messages_project ON public.messages(project_id);
CREATE INDEX IF NOT EXISTS idx_messages_created ON public.messages(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_agent_runs_project ON public.agent_runs(project_id);
CREATE INDEX IF NOT EXISTS idx_issues_project ON public.issues(project_id);
-- ============================================
-- GRANTS
-- ============================================
GRANT USAGE ON SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated;

View File

@@ -0,0 +1,199 @@
_format_version: "2.1"
_transform: true
###
### Consumers / JWT Credentials
###
consumers:
- username: DASHBOARD
- username: anon
keyauth_credentials:
- key: ${SUPABASE_ANON_KEY}
- username: service_role
keyauth_credentials:
- key: ${SUPABASE_SERVICE_KEY}
###
### Access Control Lists
###
acls:
- consumer: anon
group: anon
- consumer: service_role
group: admin
###
### Dashboard Authentication
###
basicauth_credentials:
- consumer: DASHBOARD
username: ${DASHBOARD_USERNAME}
password: ${DASHBOARD_PASSWORD}
###
### API Routes
###
services:
## Open routes (no auth)
- name: auth-v1-open
url: http://supabase-auth:9999/verify
routes:
- name: auth-v1-open
strip_path: true
paths:
- /auth/v1/verify
plugins:
- name: cors
- name: auth-v1-open-callback
url: http://supabase-auth:9999/callback
routes:
- name: auth-v1-open-callback
strip_path: true
paths:
- /auth/v1/callback
plugins:
- name: cors
- name: auth-v1-open-authorize
url: http://supabase-auth:9999/authorize
routes:
- name: auth-v1-open-authorize
strip_path: true
paths:
- /auth/v1/authorize
plugins:
- name: cors
## Auth routes
- name: auth-v1
_comment: "GoTrue: /auth/v1/* -> http://supabase-auth:9999/*"
url: http://supabase-auth:9999/
routes:
- name: auth-v1-all
strip_path: true
paths:
- /auth/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
## REST API routes
- name: rest-v1
_comment: "PostgREST: /rest/v1/* -> http://supabase-rest:3000/*"
url: http://supabase-rest:3000/
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
## GraphQL routes
- name: graphql-v1
_comment: "pg_graphql: /graphql/v1 -> http://supabase-rest:3000/rpc/graphql"
url: http://supabase-rest:3000/rpc/graphql
routes:
- name: graphql-v1-all
strip_path: true
paths:
- /graphql/v1
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
## Realtime routes
- name: realtime-v1-ws
_comment: "Realtime: /realtime/v1/* -> ws://supabase-realtime:4000/socket/*"
url: http://supabase-realtime:4000/socket/
routes:
- name: realtime-v1-ws-all
strip_path: true
paths:
- /realtime/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
## Storage routes
- name: storage-v1
_comment: "Storage: /storage/v1/* -> http://supabase-storage:5000/*"
url: http://supabase-storage:5000/
routes:
- name: storage-v1-all
strip_path: true
paths:
- /storage/v1/
plugins:
- name: cors
## Functions routes
- name: functions-v1
_comment: "Functions: /functions/v1/* -> http://supabase-functions:9000/*"
url: http://supabase-functions:9000/
routes:
- name: functions-v1-all
strip_path: true
paths:
- /functions/v1/
plugins:
- name: cors
## Analytics routes
- name: analytics-v1
_comment: "Logflare: /analytics/v1/* -> http://supabase-analytics:4000/*"
url: http://supabase-analytics:4000/
routes:
- name: analytics-v1-all
strip_path: true
paths:
- /analytics/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
## Meta routes
- name: meta
_comment: "PG Meta: /pg/* -> http://supabase-meta:8080/*"
url: http://supabase-meta:8080/
routes:
- name: meta-all
strip_path: true
paths:
- /pg/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
## Dashboard (Studio)
- name: dashboard
_comment: "Studio: /* -> http://supabase-studio:3000/*"
url: http://supabase-studio:3000/
routes:
- name: dashboard-all
strip_path: true
paths:
- /
plugins:
- name: cors
- name: basic-auth
config:
hide_credentials: true

View File

@@ -0,0 +1,46 @@
// Main edge function entry point
import { serve } from "https://deno.land/std@0.177.0/http/server.ts"
serve(async (req: Request) => {
const url = new URL(req.url)
const pathname = url.pathname
// Route to different functions based on path
if (pathname === "/trigger-n8n" || pathname === "/functions/v1/trigger-n8n") {
return await handleTriggerN8n(req)
}
if (pathname === "/n8n-callback" || pathname === "/functions/v1/n8n-callback") {
return await handleN8nCallback(req)
}
return new Response(JSON.stringify({
message: "Mylder Edge Functions",
available: ["/trigger-n8n", "/n8n-callback"]
}), {
headers: { "Content-Type": "application/json" },
status: 200
})
})
async function handleTriggerN8n(req: Request): Promise<Response> {
// Will be implemented when connecting to n8n
return new Response(JSON.stringify({
status: "placeholder",
message: "trigger-n8n function - to be implemented"
}), {
headers: { "Content-Type": "application/json" },
status: 200
})
}
async function handleN8nCallback(req: Request): Promise<Response> {
// Will be implemented when connecting to n8n
return new Response(JSON.stringify({
status: "placeholder",
message: "n8n-callback function - to be implemented"
}), {
headers: { "Content-Type": "application/json" },
status: 200
})
}