initial
This commit is contained in:
Executable
+22
@@ -0,0 +1,22 @@
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: python3 gen-jwt.py <jwt_secret>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
secret = sys.argv[1]
|
||||
b = lambda d: base64.urlsafe_b64encode(d).rstrip(b"=").decode()
|
||||
iat = int(time.time())
|
||||
exp = iat + 315360000 # 10 years
|
||||
|
||||
for role in ["anon", "service_role"]:
|
||||
h = b(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
|
||||
p = b(json.dumps({"role": role, "iss": "supabase", "iat": iat, "exp": exp}, separators=(",", ":")).encode())
|
||||
s = b(hmac.new(secret.encode(), (h + "." + p).encode(), hashlib.sha256).digest())
|
||||
print(role + ": " + h + "." + p + "." + s)
|
||||
@@ -0,0 +1,87 @@
|
||||
# Production scripts
|
||||
|
||||
Helper scripts that talk to the Hetzner VPS hosting Supabase + LiveKit.
|
||||
All commands read shared config from `config.sh`.
|
||||
|
||||
## Setup (once)
|
||||
|
||||
1. Copy your SSH key to the server so scripts don't prompt for a password:
|
||||
```
|
||||
ssh-keygen -t ed25519 # only if you don't already have one
|
||||
ssh-copy-id prox@46.225.156.249
|
||||
ssh prox@46.225.156.249 'echo ok'
|
||||
```
|
||||
2. Make the scripts executable:
|
||||
```
|
||||
chmod +x scripts/prod/*.sh
|
||||
```
|
||||
3. Edit `scripts/prod/config.sh` when the server IP or domains change.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `push-migrations.sh [<filter>]`
|
||||
Uploads `supabase/migrations/*.sql` to `/tmp/migrations` on the server and
|
||||
runs each file through `docker compose exec db psql`. Pass an optional
|
||||
filter (substring match) to only apply specific timestamps.
|
||||
|
||||
### `push-edge-function.sh <name>`
|
||||
Uploads `supabase/functions/<name>/` to
|
||||
`/opt/supabase/volumes/functions/<name>/` and restarts the edge-runtime
|
||||
container.
|
||||
|
||||
### `gen-jwt.sh <jwt-secret>`
|
||||
Wrapper around `scripts/gen-jwt.py`. Prints fresh 10-year `anon` and
|
||||
`service_role` HS256 JWTs for the given secret. Paste the results into
|
||||
`/opt/supabase/.env` (`ANON_KEY` / `SERVICE_ROLE_KEY`) and the desktop
|
||||
`.env` (`SUPABASE_ANON_KEY`).
|
||||
|
||||
### `create-invite.sh <code> [<uses>] [<days>]`
|
||||
`INSERT`s a signup-invite row into `public.invites`. Defaults: 50 uses,
|
||||
365-day expiry.
|
||||
|
||||
### `logs.sh <stack> [<service>] [<tail-lines>]`
|
||||
Follows logs from either the `supabase` or `livekit` compose stack.
|
||||
|
||||
### `restart.sh <stack> [<service>]`
|
||||
Restarts one service, or (without a service) brings the whole stack down
|
||||
and back up.
|
||||
|
||||
### `tunnel-db.sh [<local-port>]`
|
||||
Opens `ssh -L <local>:localhost:5432`. Default local port is `5433`.
|
||||
Leave it running while you use `psql` / `supabase db push` / etc.
|
||||
|
||||
### `tunnel-mailpit.sh [<local-port>]`
|
||||
Opens a tunnel to the Mailpit web UI. Default local port is `8025`.
|
||||
|
||||
### `rotate-livekit-keys.sh`
|
||||
Generates a fresh LiveKit API key + secret, rewrites the `keys:` block in
|
||||
`/opt/livekit/livekit.yaml`, and restarts the SFU. After running, also
|
||||
update the Supabase `.env` (`LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET`) and
|
||||
`restart.sh supabase functions` so the mint-livekit-token Edge Function
|
||||
sees the new values.
|
||||
|
||||
## Typical workflows
|
||||
|
||||
**Ship a new migration**
|
||||
```
|
||||
# add supabase/migrations/20260501_my_change.sql locally
|
||||
./scripts/prod/push-migrations.sh 20260501
|
||||
```
|
||||
|
||||
**Ship a new Edge Function change**
|
||||
```
|
||||
# edit supabase/functions/mint-livekit-token/index.ts locally
|
||||
./scripts/prod/push-edge-function.sh mint-livekit-token
|
||||
```
|
||||
|
||||
**Debug a failing magic-link**
|
||||
```
|
||||
./scripts/prod/logs.sh supabase auth 200
|
||||
./scripts/prod/tunnel-mailpit.sh # open http://localhost:8025
|
||||
```
|
||||
|
||||
**Rotate LiveKit credentials**
|
||||
```
|
||||
./scripts/prod/rotate-livekit-keys.sh
|
||||
# follow the printed hint to update supabase .env + restart functions
|
||||
```
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared config for all prod scripts. Source this in each script:
|
||||
# source "$(dirname "$0")/config.sh"
|
||||
#
|
||||
# Customize here when the server IP / domains change — all other scripts pick
|
||||
# the values up automatically.
|
||||
|
||||
export PROD_SERVER="46.225.156.249"
|
||||
export PROD_USER="prox"
|
||||
|
||||
# Paths on the remote server.
|
||||
export PROD_SUPABASE_DIR="/opt/supabase"
|
||||
export PROD_LIVEKIT_DIR="/opt/livekit"
|
||||
|
||||
# Public domains (served via Caddy on the same VPS).
|
||||
export PROD_DOMAIN_SUPABASE="supabase.netralax.cloud"
|
||||
export PROD_DOMAIN_LIVEKIT="livekit.netralax.cloud"
|
||||
|
||||
# SSH helper: forwards the standard `-o StrictHostKeyChecking=accept-new` so
|
||||
# first connections don't prompt. Override SSH_OPTS from the environment if
|
||||
# you need a jumphost etc.
|
||||
export SSH_OPTS="${SSH_OPTS:--o StrictHostKeyChecking=accept-new}"
|
||||
export SSH_HOST="${PROD_USER}@${PROD_SERVER}"
|
||||
|
||||
remote() {
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_OPTS} "${SSH_HOST}" "$@"
|
||||
}
|
||||
|
||||
remote_tty() {
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_OPTS} -t "${SSH_HOST}" "$@"
|
||||
}
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Insert a new signup-invite code into prod Supabase. Runs `INSERT INTO
|
||||
# public.invites ...` via `docker exec psql`. Code is validated by the
|
||||
# `handle_new_user` trigger when a new user registers.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/create-invite.sh <code> [<uses_limit>] [<expires_days>]
|
||||
# ./scripts/prod/create-invite.sh FRIENDS 20 365
|
||||
#
|
||||
# Defaults: uses_limit=50, expires_days=365
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
code="${1:-}"
|
||||
uses_limit="${2:-50}"
|
||||
expires_days="${3:-365}"
|
||||
|
||||
if [[ -z "${code}" ]]; then
|
||||
echo "usage: $0 <code> [<uses_limit>=50] [<expires_days>=365]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ${#code} -lt 4 || ${#code} -gt 64 ]]; then
|
||||
echo "code length must be between 4 and 64 chars (got ${#code})" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sql="INSERT INTO public.invites (code, uses_limit, expires_at, disabled) \
|
||||
VALUES ('${code}', ${uses_limit}, now() + interval '${expires_days} days', false) \
|
||||
RETURNING code, uses_limit, expires_at;"
|
||||
|
||||
echo "creating invite '${code}' (uses=${uses_limit}, expires=${expires_days}d)"
|
||||
remote "cd ${PROD_SUPABASE_DIR} && docker compose exec -T db psql -U postgres -d postgres -c \"${sql}\""
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Thin wrapper around scripts/gen-jwt.py that generates HS256 anon +
|
||||
# service_role JWTs for a given JWT_SECRET. The keys are 10-year, iss=supabase.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/gen-jwt.sh <jwt-secret>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "${here}/../.." && pwd)"
|
||||
|
||||
secret="${1:-}"
|
||||
if [[ -z "${secret}" ]]; then
|
||||
echo "usage: $0 <jwt-secret>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 "${repo_root}/scripts/gen-jwt.py" "${secret}"
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Tail logs from a service in either the Supabase or LiveKit compose stack.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/logs.sh <stack> [<service>] [<tail>]
|
||||
# stack = supabase | livekit
|
||||
# service = (optional) single service, default = all
|
||||
# tail = (optional) line count, default = 100
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/prod/logs.sh supabase # all services
|
||||
# ./scripts/prod/logs.sh supabase auth # just auth
|
||||
# ./scripts/prod/logs.sh supabase auth 300 # auth, last 300 lines
|
||||
# ./scripts/prod/logs.sh livekit livekit 200
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
stack="${1:-}"
|
||||
service="${2:-}"
|
||||
tail_n="${3:-100}"
|
||||
|
||||
case "${stack}" in
|
||||
supabase) dir="${PROD_SUPABASE_DIR}" ;;
|
||||
livekit) dir="${PROD_LIVEKIT_DIR}" ;;
|
||||
*)
|
||||
echo "usage: $0 <supabase|livekit> [service] [tail-lines]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cmd="cd ${dir} && docker compose logs --tail=${tail_n} -f"
|
||||
if [[ -n "${service}" ]]; then
|
||||
cmd+=" ${service}"
|
||||
fi
|
||||
|
||||
remote_tty "${cmd}"
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Deploy a Supabase Edge Function to the prod self-hosted stack.
|
||||
# Uploads `supabase/functions/<name>/` to
|
||||
# `${PROD_SUPABASE_DIR}/volumes/functions/<name>/` and restarts the
|
||||
# edge-runtime container so it picks up the new code.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/push-edge-function.sh <function-name>
|
||||
# ./scripts/prod/push-edge-function.sh mint-livekit-token
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
name="${1:-}"
|
||||
if [[ -z "${name}" ]]; then
|
||||
echo "usage: $0 <function-name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "${here}/../.." && pwd)"
|
||||
local_dir="${repo_root}/supabase/functions/${name}"
|
||||
|
||||
if [[ ! -d "${local_dir}" ]]; then
|
||||
echo "function dir not found: ${local_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remote_dir="${PROD_SUPABASE_DIR}/volumes/functions/${name}"
|
||||
|
||||
echo "syncing ${local_dir} → ${SSH_HOST}:${remote_dir}"
|
||||
remote "mkdir -p ${remote_dir}"
|
||||
# shellcheck disable=SC2086
|
||||
scp ${SSH_OPTS} -r "${local_dir}/." "${SSH_HOST}:${remote_dir}/"
|
||||
|
||||
echo
|
||||
echo "restarting edge-runtime"
|
||||
remote "cd ${PROD_SUPABASE_DIR} && docker compose up -d functions"
|
||||
remote "cd ${PROD_SUPABASE_DIR} && docker compose logs functions --tail=10"
|
||||
|
||||
echo
|
||||
echo "done — function '${name}' deployed."
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Ship local `supabase/migrations/*.sql` to the prod DB and apply them in
|
||||
# alphabetical order (= timestamp order given Supabase's filename convention).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/push-migrations.sh # apply all migrations
|
||||
# ./scripts/prod/push-migrations.sh 20260501_foo # apply only matching files
|
||||
#
|
||||
# Migrations should be idempotent (`IF NOT EXISTS` / `OR REPLACE`) — the
|
||||
# script stops on the first failing statement so you notice drift immediately.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
repo_root="$(cd "${here}/../.." && pwd)"
|
||||
migrations_dir="${repo_root}/supabase/migrations"
|
||||
|
||||
if [[ ! -d "${migrations_dir}" ]]; then
|
||||
echo "migrations dir not found: ${migrations_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
filter="${1:-}"
|
||||
|
||||
echo "syncing ${migrations_dir} → ${SSH_HOST}:/tmp/migrations"
|
||||
remote "rm -rf /tmp/migrations && mkdir -p /tmp/migrations"
|
||||
# shellcheck disable=SC2086
|
||||
scp ${SSH_OPTS} -r "${migrations_dir}"/* "${SSH_HOST}:/tmp/migrations/"
|
||||
|
||||
echo
|
||||
echo "applying migrations via docker exec db psql"
|
||||
remote "cd ${PROD_SUPABASE_DIR} && for f in /tmp/migrations/*.sql; do \
|
||||
case \"\$(basename \"\$f\")\" in \
|
||||
*${filter}*) echo \"=== applying \$f ===\"; docker compose exec -T db psql -U postgres -d postgres -v ON_ERROR_STOP=1 < \"\$f\" ;; \
|
||||
*) echo \"=== skipping \$f (filter '${filter}') ===\" ;; \
|
||||
esac; \
|
||||
done"
|
||||
|
||||
echo
|
||||
echo "done."
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Restart services in the Supabase or LiveKit compose stack.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/restart.sh <stack> [<service>]
|
||||
# stack = supabase | livekit
|
||||
# service = (optional) restart just this one, default = whole stack
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/prod/restart.sh supabase # down + up -d
|
||||
# ./scripts/prod/restart.sh supabase auth # only auth
|
||||
# ./scripts/prod/restart.sh livekit # both caddy + livekit
|
||||
# ./scripts/prod/restart.sh livekit livekit # only livekit container
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
stack="${1:-}"
|
||||
service="${2:-}"
|
||||
|
||||
case "${stack}" in
|
||||
supabase) dir="${PROD_SUPABASE_DIR}" ;;
|
||||
livekit) dir="${PROD_LIVEKIT_DIR}" ;;
|
||||
*)
|
||||
echo "usage: $0 <supabase|livekit> [service]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n "${service}" ]]; then
|
||||
echo "restarting ${stack}/${service}"
|
||||
remote "cd ${dir} && docker compose restart ${service}"
|
||||
else
|
||||
echo "restarting full ${stack} stack (down + up -d)"
|
||||
remote "cd ${dir} && docker compose down && docker compose up -d"
|
||||
fi
|
||||
|
||||
echo
|
||||
remote "cd ${dir} && docker compose ps"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Rotate the LiveKit API-Key + Secret and restart the SFU. Prints the new
|
||||
# values so you can paste them into the Supabase Edge-Function env (the
|
||||
# mint-livekit-token function signs JWTs with them).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/rotate-livekit-keys.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
new_key="API$(openssl rand -hex 8)"
|
||||
new_secret="$(openssl rand -hex 32)"
|
||||
|
||||
echo "new LiveKit credentials:"
|
||||
echo " LIVEKIT_API_KEY=${new_key}"
|
||||
echo " LIVEKIT_API_SECRET=${new_secret}"
|
||||
echo
|
||||
read -rp "write these into ${PROD_LIVEKIT_DIR}/livekit.yaml on the server? [y/N] " confirm
|
||||
if [[ "${confirm,,}" != "y" ]]; then
|
||||
echo "aborted — nothing changed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Replace the `keys:` block (last non-empty section in our config).
|
||||
remote "cd ${PROD_LIVEKIT_DIR} && \
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
p = Path('livekit.yaml')
|
||||
lines = p.read_text().splitlines()
|
||||
out = []
|
||||
skip = False
|
||||
for line in lines:
|
||||
if line.strip().startswith('keys:'):
|
||||
out.append('keys:')
|
||||
out.append(f' ${new_key}: ${new_secret}')
|
||||
skip = True
|
||||
continue
|
||||
if skip:
|
||||
if line.startswith(' '):
|
||||
continue
|
||||
skip = False
|
||||
out.append(line)
|
||||
p.write_text('\n'.join(out) + '\n')
|
||||
print('livekit.yaml updated')
|
||||
PY"
|
||||
|
||||
remote "cd ${PROD_LIVEKIT_DIR} && docker compose restart livekit"
|
||||
echo
|
||||
echo "LiveKit restarted with new keys."
|
||||
echo
|
||||
echo "Next: push these to the Supabase Edge-Function env on the server:"
|
||||
echo " edit ${PROD_SUPABASE_DIR}/.env → set LIVEKIT_API_KEY / LIVEKIT_API_SECRET"
|
||||
echo " then: ./scripts/prod/restart.sh supabase functions"
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Open an SSH tunnel that exposes prod Postgres on localhost:5433. Useful
|
||||
# for running `psql`, `supabase db push`, or any GUI DB client against prod.
|
||||
#
|
||||
# The prod Postgres container only listens on the host's localhost:5432, so
|
||||
# we need an SSH tunnel to reach it from the Mac. Tunnel stays open until
|
||||
# Ctrl+C.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/tunnel-db.sh
|
||||
# # then in another terminal:
|
||||
# psql -h localhost -p 5433 -U postgres -d postgres
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
local_port="${1:-5433}"
|
||||
|
||||
echo "tunnel: localhost:${local_port} → ${SSH_HOST}:localhost:5432"
|
||||
echo "press Ctrl+C to close"
|
||||
# shellcheck disable=SC2086
|
||||
exec ssh ${SSH_OPTS} -N -L "${local_port}:localhost:5432" "${SSH_HOST}"
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Open an SSH tunnel that exposes the Mailpit web UI on localhost:8025.
|
||||
# Then browse to http://localhost:8025 on your Mac to see all emails the
|
||||
# auth service has sent (magic-link confirmation, password reset, etc.).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/prod/tunnel-mailpit.sh
|
||||
# # then open http://localhost:8025 in your browser.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${here}/config.sh"
|
||||
|
||||
local_port="${1:-8025}"
|
||||
|
||||
echo "tunnel: localhost:${local_port} → ${SSH_HOST}:localhost:8025"
|
||||
echo "open http://localhost:${local_port} in your browser"
|
||||
echo "press Ctrl+C to close"
|
||||
# shellcheck disable=SC2086
|
||||
exec ssh ${SSH_OPTS} -N -L "${local_port}:localhost:8025" "${SSH_HOST}"
|
||||
Reference in New Issue
Block a user