Skip to content

Commit cdc462c

Browse files
committed
Initial OSS release: EVC Team Relay v1.8.7
Self-hosted collaborative editing infrastructure for Obsidian. Features: - Real-time CRDT synchronization (y-sweet) - Document and folder sharing with permissions - Web publishing with custom domains - OAuth/OIDC integration - Webhooks, audit logs, metrics License: Apache 2.0
0 parents  commit cdc462c

436 files changed

Lines changed: 93215 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
DOMAIN_BASE=example.com
2+
ACME_EMAIL=ops@example.com
3+
POSTGRES_PASSWORD=change-me
4+
MINIO_ROOT_USER=relay
5+
MINIO_ROOT_PASSWORD=change-this-too
6+
JWT_SECRET=replace-me
7+
BOOTSTRAP_ADMIN_EMAIL=admin@example.com
8+
BOOTSTRAP_ADMIN_PASSWORD=super-secret-pass
9+
RELAY_PUBLIC_URL=wss://relay.${DOMAIN_BASE}
10+
11+
# Server identity (for multi-server plugin support)
12+
SERVER_NAME=My Relay Server
13+
# SERVER_ID=my-server-id # Optional, defaults to relay_key_id
14+
15+
# Logging configuration
16+
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
17+
LOG_FORMAT=json # json or text
18+
19+
# PostgreSQL backup configuration
20+
BACKUP_RETENTION=7 # Days to keep local backups
21+
BACKUP_HOUR=2 # Hour to run daily backup (0-23, UTC)
22+
BACKUP_RUN_ON_STARTUP=true # Run backup on container startup
23+
# BACKUP_S3_ENABLED=false # Enable S3/MinIO upload
24+
# BACKUP_S3_BUCKET=relay-backups # S3 bucket name
25+
26+
# Grafana configuration
27+
GRAFANA_ADMIN_USER=admin
28+
GRAFANA_ADMIN_PASSWORD=change-me-please
29+
30+
# Email configuration (for notifications)
31+
EMAIL_ENABLED=false # Enable email sending
32+
SMTP_HOST= # SMTP server hostname
33+
SMTP_PORT=587 # SMTP port (587 for TLS, 465 for SSL)
34+
SMTP_USER= # SMTP username
35+
SMTP_PASSWORD= # SMTP password
36+
SMTP_USE_TLS=true # Use STARTTLS
37+
EMAIL_FROM=noreply@example.com # From address
38+
EMAIL_REPLY_TO= # Reply-to address (optional)
39+
40+
# Background worker configuration
41+
WEBHOOK_WORKER_INTERVAL=30 # Webhook worker poll interval (seconds)
42+
WEBHOOK_WORKER_BATCH_SIZE=50 # Max webhooks to process per cycle
43+
EMAIL_WORKER_INTERVAL=60 # Email worker poll interval (seconds)
44+
EMAIL_WORKER_BATCH_SIZE=100 # Max emails to process per cycle
45+
46+
# Web Publishing configuration (v1.7)
47+
WEB_PUBLISH_DOMAIN= # Web publishing domain (e.g., docs.example.com)
48+
# Leave empty to disable web publishing
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Agent Bootstrap (Cursor)
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
prompt_file:
7+
description: "Prompt file path"
8+
required: true
9+
default: "PROMPTS/epic-control-plane-v0.md"
10+
type: choice
11+
options:
12+
- "PROMPTS/bootstrap.md"
13+
- "PROMPTS/epic-control-plane-v0.md"
14+
model:
15+
description: "Cursor model"
16+
required: true
17+
default: "gpt-5.1-codex"
18+
type: choice
19+
options:
20+
- "auto"
21+
- "gpt-5.1-codex"
22+
- "gpt-5.2"
23+
- "gpt-5.1"
24+
25+
permissions:
26+
contents: write
27+
pull-requests: write
28+
29+
jobs:
30+
bootstrap:
31+
runs-on: ubuntu-latest
32+
environment: staging
33+
steps:
34+
- uses: actions/checkout@v4
35+
with:
36+
fetch-depth: 0
37+
38+
- name: Install Cursor CLI
39+
run: |
40+
curl https://cursor.com/install -fsS | bash
41+
echo "$HOME/.cursor/bin" >> $GITHUB_PATH
42+
- name: Run Cursor Agent
43+
env:
44+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
45+
run: |
46+
cursor-agent -p "$(cat '${{ inputs.prompt_file }}')" --model "${{ inputs.model }}"
47+
- name: Configure git identity
48+
run: |
49+
git config --global user.name "github-actions[bot]"
50+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
51+
52+
- name: Create PR with changes
53+
env:
54+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55+
run: |
56+
set -e
57+
if git diff --quiet; then
58+
echo "No changes from agent."
59+
exit 0
60+
fi
61+
BR="bootstrap/${{ github.run_id }}"
62+
git checkout -b "$BR"
63+
git add -A
64+
git commit -m "bootstrap: scaffold relay-onprem stack"
65+
git push origin "$BR"
66+
gh pr create --title "bootstrap: scaffold stack" --body "Generated by Cursor agent" --base main --head "$BR"

.github/workflows/ci.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
paths:
9+
- 'apps/**'
10+
- 'infra/**'
11+
- '.github/workflows/ci.yml'
12+
13+
jobs:
14+
lint-python:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v4
21+
with:
22+
version: "latest"
23+
24+
- name: Set up Python
25+
run: uv python install 3.12
26+
27+
- name: Lint with Ruff
28+
working-directory: apps/control-plane
29+
run: |
30+
uv sync --extra dev --extra test
31+
uv run ruff check app/ tests/
32+
uv run ruff format --check app/ tests/
33+
34+
test-python:
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- name: Install uv
40+
uses: astral-sh/setup-uv@v4
41+
with:
42+
version: "latest"
43+
44+
- name: Set up Python
45+
run: uv python install 3.12
46+
47+
- name: Run tests
48+
working-directory: apps/control-plane
49+
run: |
50+
uv sync --extra dev --extra test
51+
uv run pytest -v --tb=short
52+
53+
build-docker:
54+
runs-on: ubuntu-latest
55+
needs: [lint-python, test-python]
56+
steps:
57+
- uses: actions/checkout@v4
58+
59+
- name: Set up Docker Buildx
60+
uses: docker/setup-buildx-action@v3
61+
62+
- name: Build control-plane
63+
uses: docker/build-push-action@v6
64+
with:
65+
context: apps/control-plane
66+
push: false
67+
tags: evc-team-relay/control-plane:test
68+
cache-from: type=gha
69+
cache-to: type=gha,mode=max
70+
71+
- name: Build web-publish
72+
uses: docker/build-push-action@v6
73+
with:
74+
context: apps/web-publish
75+
push: false
76+
tags: evc-team-relay/web-publish:test
77+
cache-from: type=gha
78+
cache-to: type=gha,mode=max

.github/workflows/deploy.yml

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
name: Deploy (staging)
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
7+
jobs:
8+
deploy:
9+
runs-on: ubuntu-latest
10+
environment: staging
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Setup SSH
16+
run: |
17+
mkdir -p ~/.ssh
18+
chmod 700 ~/.ssh
19+
echo "${{ secrets.DEPLOY_SSH_KEY_B64 }}" | base64 -d > ~/.ssh/id_ed25519
20+
chmod 600 ~/.ssh/id_ed25519
21+
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
22+
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts
23+
24+
- name: Generate infra/.env from secrets
25+
run: |
26+
cat > infra/.env <<EOF
27+
DOMAIN_BASE=${{ secrets.DOMAIN_BASE }}
28+
ACME_EMAIL=${{ secrets.ACME_EMAIL }}
29+
30+
# Postgres (control-plane)
31+
POSTGRES_USER=relaycp
32+
POSTGRES_DB=relaycp
33+
POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
34+
DATABASE_URL=postgresql+psycopg://relaycp:${{ secrets.POSTGRES_PASSWORD }}@postgres:5432/relaycp
35+
36+
# MinIO
37+
MINIO_ROOT_USER=${{ secrets.MINIO_ROOT_USER }}
38+
MINIO_ROOT_PASSWORD=${{ secrets.MINIO_ROOT_PASSWORD }}
39+
40+
# Control-plane auth/bootstrap
41+
JWT_SECRET=${{ secrets.JWT_SECRET }}
42+
BOOTSTRAP_ADMIN_EMAIL=${{ secrets.BOOTSTRAP_ADMIN_EMAIL }}
43+
BOOTSTRAP_ADMIN_PASSWORD=${{ secrets.BOOTSTRAP_ADMIN_PASSWORD }}
44+
45+
# Relay public URL (for clients)
46+
RELAY_PUBLIC_URL=wss://relay.${{ secrets.DOMAIN_BASE }}
47+
48+
# Relay token signing (Ed25519)
49+
RELAY_PRIVATE_KEY=$(echo "${{ secrets.RELAY_PRIVATE_KEY }}" | base64 -w 0)
50+
RELAY_KEY_ID=${{ secrets.RELAY_KEY_ID }}
51+
52+
# OAuth/OIDC Configuration
53+
OAUTH_ENABLED=${{ secrets.OAUTH_ENABLED }}
54+
OAUTH_PROVIDER_NAME=${{ secrets.OAUTH_PROVIDER_NAME }}
55+
OAUTH_ISSUER_URL=${{ secrets.OAUTH_ISSUER_URL }}
56+
OAUTH_CLIENT_ID=${{ secrets.OAUTH_CLIENT_ID }}
57+
OAUTH_CLIENT_SECRET=${{ secrets.OAUTH_CLIENT_SECRET }}
58+
OAUTH_AUTO_REGISTER=${{ secrets.OAUTH_AUTO_REGISTER }}
59+
60+
# Web Publishing (optional)
61+
WEB_PUBLISH_DOMAIN=${{ secrets.WEB_PUBLISH_DOMAIN }}
62+
EOF
63+
64+
- name: Extract relay public key from private key
65+
run: |
66+
cat > extract_pubkey.py <<'PYTHON'
67+
import base64
68+
import os
69+
from cryptography.hazmat.primitives import serialization
70+
71+
private_pem = os.environ['RELAY_PRIVATE_KEY']
72+
private_key = serialization.load_pem_private_key(
73+
private_pem.encode('utf-8'),
74+
password=None
75+
)
76+
public_bytes = private_key.public_key().public_bytes(
77+
encoding=serialization.Encoding.Raw,
78+
format=serialization.PublicFormat.Raw
79+
)
80+
public_base64 = base64.b64encode(public_bytes).decode('utf-8')
81+
print(public_base64)
82+
PYTHON
83+
84+
pip install cryptography
85+
export RELAY_PUBLIC_KEY=$(python extract_pubkey.py)
86+
echo "RELAY_PUBLIC_KEY=$RELAY_PUBLIC_KEY" >> $GITHUB_ENV
87+
env:
88+
RELAY_PRIVATE_KEY: ${{ secrets.RELAY_PRIVATE_KEY }}
89+
90+
- name: Generate relay.toml from secrets
91+
run: |
92+
mkdir -p infra/relay
93+
cat > infra/relay/relay.toml <<EOF
94+
[server]
95+
url = "https://relay.${{ secrets.DOMAIN_BASE }}"
96+
host = "0.0.0.0"
97+
port = 8080
98+
99+
[metrics]
100+
port = 9090
101+
102+
[logging]
103+
level = "info"
104+
format = "pretty"
105+
106+
# Control-plane authentication
107+
[[auth]]
108+
key_id = "${{ secrets.RELAY_KEY_ID }}"
109+
public_key = "${{ env.RELAY_PUBLIC_KEY }}"
110+
111+
[store]
112+
type = "minio"
113+
bucket = "relay"
114+
endpoint = "http://minio:9000"
115+
access_key = "${{ secrets.MINIO_ROOT_USER }}"
116+
secret_key = "${{ secrets.MINIO_ROOT_PASSWORD }}"
117+
prefix = ""
118+
EOF
119+
120+
- name: Sync repo to VPS (exclude runtime data)
121+
run: |
122+
rsync -az --delete \
123+
--exclude ".git/" \
124+
--exclude "infra/data/" \
125+
./ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:~/relay-onprem/"
126+
127+
- name: Compose up + smoke checks
128+
run: |
129+
ssh "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" <<'SSH'
130+
set -euo pipefail
131+
cd ~/relay-onprem/infra
132+
133+
docker compose up -d --build || {
134+
echo "compose up failed — dumping logs"
135+
docker compose ps -a || true
136+
docker compose logs --tail=200 postgres control-plane-migrate control-plane caddy || true
137+
exit 1
138+
}
139+
docker compose ps
140+
141+
retries=10
142+
delay=6
143+
144+
check_cp() {
145+
docker compose exec -T control-plane python -c 'import sys,urllib.request; r=urllib.request.urlopen("http://control-plane:8000/health",timeout=5); sys.exit(0 if r.status==200 else 1)'
146+
}
147+
148+
check_relay() {
149+
docker compose exec -T control-plane python -c 'import sys,urllib.request; r=urllib.request.urlopen("http://relay-server:9090/metrics",timeout=5); r.read(200); sys.exit(0 if r.status==200 else 1)'
150+
}
151+
152+
n=0
153+
until check_cp; do
154+
n=$((n+1))
155+
if [ "$n" -ge "$retries" ]; then
156+
echo "control-plane health failed after $n attempts"
157+
docker compose ps
158+
docker compose logs --tail=200 caddy control-plane
159+
exit 1
160+
fi
161+
sleep "$delay"
162+
done
163+
164+
n=0
165+
until check_relay; do
166+
n=$((n+1))
167+
if [ "$n" -ge "$retries" ]; then
168+
echo "relay-server health failed after $n attempts"
169+
docker compose ps
170+
docker compose logs --tail=200 caddy relay-server
171+
exit 1
172+
fi
173+
sleep "$delay"
174+
done
175+
SSH

0 commit comments

Comments
 (0)