dev: add parallel dev environment under /docker/dev

Near-1:1 clone of the prod remote-access stack, isolated on a new external
dev_edge network and fronted by the same shared Caddy instance (dual-homed on
edge + dev_edge). Dev is manual-start (not on boot).

- Hostnames: app-dev / api-dev .linumiq.net, tunnels under *.dev.linumiq.net,
  dev tunnel ingress on port 7001.
- Dev Supabase (project supabase-dev, *-dev containers), web, frps, redis,
  stripe-stub, bandwidth-worker with fresh independent secrets (gitignored).
- Shared Caddyfile: app-dev -> web-dev, api-dev -> dev kong (+webhook block),
  *.dev -> frps-dev vhost. Caddy compose dual-homed on dev_edge.
- On-demand-TLS authorizer (prod check-subdomain, in gitignored volumes/)
  extended additively: app-dev/api-dev -> 200; *.dev delegated to the dev
  authorizer. Prod allow-list logic unchanged.
- dev.sh manual up/down/ps helper; README documents topology + secrets.

Secrets, frps.toml, volumes/, web worktree and data dirs are gitignored.
This commit is contained in:
2026-05-30 13:23:34 +02:00
parent 50ab46dbe1
commit 7fe0cc3753
25 changed files with 1473 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
DOMAIN=linumiq.net
EDGE_STRIPE_WEBHOOK_URL=http://supabase-edge-functions:9000/stripe-webhook
STRIPE_STUB_WEBHOOK_SECRET=__SECRET__
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 4242
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "4242"]
+63
View File
@@ -0,0 +1,63 @@
"""stripe-stub: pretends every checkout succeeds and forwards test webhooks.
Forwarded webhooks are signed with an HMAC-SHA256 signature so the edge
function can reject unsigned/forged/replayed events.
"""
import hashlib
import hmac
import os
import time
import uuid
from typing import Any
import httpx
from fastapi import FastAPI, Request
DOMAIN = os.environ.get("DOMAIN", "linumiq.net")
EDGE_URL = os.environ.get(
"EDGE_STRIPE_WEBHOOK_URL",
"http://supabase-edge-functions:9000/stripe-webhook",
)
WEBHOOK_SECRET = os.environ.get("STRIPE_STUB_WEBHOOK_SECRET", "")
app = FastAPI()
def sign(body: bytes) -> str:
ts = int(time.time())
mac = hmac.new(
WEBHOOK_SECRET.encode(),
f"{ts}.".encode() + body,
hashlib.sha256,
).hexdigest()
return f"t={ts},v1={mac}"
@app.post("/v1/checkout/sessions")
async def create_session(_request: Request) -> dict[str, str]:
session_id = f"stub_{uuid.uuid4().hex}"
return {
"id": session_id,
"url": f"https://app.{DOMAIN}/billing/success?session={session_id}",
}
@app.post("/v1/webhooks/test")
async def forward_test(request: Request) -> Any:
body = await request.body()
headers = {
"content-type": request.headers.get("content-type", "application/json"),
"x-stub-signature": sign(body),
}
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(EDGE_URL, content=body, headers=headers)
try:
return resp.json()
except Exception:
return {"upstream_status": resp.status_code, "body": resp.text}
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
+21
View File
@@ -0,0 +1,21 @@
name: stripe-stub-dev
services:
stripe-stub:
build: .
image: stripe-stub-dev:1.0.0
container_name: stripe-stub-dev
restart: unless-stopped
security_opt:
- no-new-privileges:true
env_file: .env
networks:
- dev_edge
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:4242/health').status==200 else 1)"]
interval: 30s
timeout: 5s
retries: 3
networks:
dev_edge:
external: true
+3
View File
@@ -0,0 +1,3 @@
fastapi==0.115.4
uvicorn[standard]==0.32.0
httpx==0.27.2