The learning platform for arkandia.co. Lives at campus.arkandia.co.
Users who have purchased workshops can log in, watch recorded sessions, download resources, and track their progress.
- Backend: FastAPI + asyncpg (Python 3.12), hexagonal architecture
- Frontend: Next.js 16 + Tailwind CSS 4 (TypeScript strict)
- Auth: Self-hosted Supabase Auth (GoTrue + Kong)
- Database: Self-hosted PostgreSQL (supabase/postgres image)
- Infra: Docker + Caddy on Hetzner
Prerequisites: Docker, pnpm (for running frontend checks locally), uv (for running backend checks locally)
-
Copy env file:
cp .env.example .env # The defaults work out of the box for dev. # Change POSTGRES_PASSWORD and SUPABASE_JWT_SECRET for production.
-
Start all services (Postgres, GoTrue, Kong, backend, frontend):
make dev
-
In a separate terminal, initialize the database (first time only):
make db-init # creates business tables + applies campus migrations make db-seed # optional: sample content (edit cohort_id in seed.sql first)
-
Open the app:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8000
- Supabase Auth (Kong): http://localhost:8443
-
After registering a user, link them to a client record:
make db-psql # Then run: # UPDATE core_client SET auth_user_id = '<id from auth.users>' WHERE email = '<email>';
If you change SUPABASE_JWT_SECRET, regenerate the anon key:
cd backend && uv run python3 -c "
import jwt, time
secret = 'your-new-jwt-secret-here'
print(jwt.encode({'role':'anon','iss':'supabase','iat':int(time.time()),'exp':int(time.time())+315360000}, secret, algorithm='HS256'))
"Paste the output into both SUPABASE_ANON_KEY and NEXT_PUBLIC_SUPABASE_ANON_KEY in .env.
make lint # ruff + eslint
make typecheck # pyright + tsc
make test # pytest + vitest
make check # all threecd backend
uv run pytest -v
uv run ruff check app tests
uv run pyrightcd frontend
pnpm eslint src
pnpm tsc --noEmit
pnpm vitest run-
Build images on the server (or push from CI):
docker build -t arkandia-campus-backend ./backend docker build -t arkandia-campus-frontend ./frontend
-
On the Hetzner VPS, create
.envwith production values:- Strong
POSTGRES_PASSWORDandSUPABASE_JWT_SECRET - Regenerated
SUPABASE_ANON_KEY SUPABASE_URL=https://campus.arkandia.coSITE_URL=https://campus.arkandia.coNEXT_PUBLIC_SUPABASE_URL=https://campus.arkandia.co- SMTP settings for email confirmation (
GOTRUE_SMTP_*)
- Strong
-
Start:
cd /opt/arkandia-campus docker compose -f infra/docker-compose.prod.yml up -d -
Caddy handles HTTPS automatically via Let's Encrypt.
backend/app/
├── domain/ # Pure business logic — no framework deps
│ ├── models/ # Pydantic schemas
│ ├── repositories/ # Abstract interfaces (Protocol)
│ └── services/ # Business logic calling repos
└── infrastructure/ # Framework-aware implementations
├── auth/ # JWT validation
├── persistence/ # asyncpg SQL repositories
└── routers/ # FastAPI HTTP handlers
Domain never imports from infrastructure. Infrastructure implements domain Protocols.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/health |
No | Health check |
| GET | /api/v1/catalog |
Yes | List purchased offerings |
| GET | /api/v1/catalog/{id} |
Yes | Offering detail + content |
| POST | /api/v1/progress |
Yes | Mark content complete |
| DELETE | /api/v1/progress/{id} |
Yes | Unmark content |