Compare commits
10 Commits
0abed143c5
...
5ea19bba26
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ea19bba26 | |||
| cb3a98e5e0 | |||
| 597a43b589 | |||
| e170cefb5d | |||
| b3fe2e4dac | |||
| c75edbfb8a | |||
| 7c6dbef443 | |||
| ee80f9a10f | |||
| 67979d3cd1 | |||
| c9113c9293 |
84
supabase/.env.example
Normal file
84
supabase/.env.example
Normal 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
23
supabase/bootstrap.sh
Normal 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
192
supabase/deploy.sh
Normal 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
368
supabase/docker-compose.yml
Normal 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
332
supabase/schema.sql
Normal 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;
|
||||
199
supabase/volumes/api/kong.yml
Normal file
199
supabase/volumes/api/kong.yml
Normal 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
|
||||
46
supabase/volumes/functions/main/index.ts
Normal file
46
supabase/volumes/functions/main/index.ts
Normal 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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user