- Go 47.9%
- TypeScript 45.9%
- Astro 4.4%
- Makefile 0.7%
- JavaScript 0.5%
- Other 0.6%
| .forgejo/workflows | ||
| backend | ||
| docs | ||
| frontend | ||
| nginx | ||
| .env.example | ||
| .env.stage.example | ||
| .gitignore | ||
| .golangci.yml | ||
| authentik.bootstrap.yaml | ||
| authentik.sync.yaml | ||
| compose.prod.yml | ||
| compose.stage.yml | ||
| compose.yml | ||
| go.work | ||
| Makefile | ||
| nginx.conf | ||
| README.md | ||
ArcaneDemesne WorkSuite
Go BFF API + Astro frontend for multi-tenant project management. Edge nginx on host 8084; API 5084; Astro 4084. Production: https://worksuite.arcanedemesne.com via Gateway.
Port matrix: Gateway/PORT_MATRIX.md
Compose
Requires gateway-hub (edge only) and authentik-net (API OIDC + SMTP to mailpit) — create once from Gateway. Never put api/app on gateway-hub.
cp .env.example .env # fill AUTH_SESSION_SECRET, AUTH_OIDC_*, etc.
make up
Production (auth.arcanedemesne.com authority, tier-2 ports):
cp .env.example .env.prod
# Tier 2: WORKSUITE_*_PORT 8284/5284/4284/6284; AUTH_OIDC_AUTHORITY=https://auth.arcanedemesne.com/...
# INVITE_WEB_URL=https://worksuite.arcanedemesne.com
make up-prod
Stage (stage.worksuite.arcanedemesne.com, tier-1 ports):
cp .env.stage.example .env.stage
make up-stage
make migrate-up-stage
Env files
| File | Use |
|---|---|
.env.example |
Dev Podman (worksuite-dev, tier-0 ports) |
.env |
Local dev stack (make up) |
.env.stage.example |
Stage template (worksuite-stage) |
.env.stage |
Stage stack (make up-stage) |
.env.prod |
Production stack (make up-prod, project worksuite) |
backend/.env.local |
Host split-port dev (make run, bun dev) |
Authentik: make authentik-bootstrap, docs/SSO.md.
Local dev (host, split ports)
| Process | Directory | Env file | URL |
|---|---|---|---|
| Go API | backend/ |
.env.local (from .env.example) |
http://localhost:5084 |
| Astro | frontend/worksuite/ |
src/.env.development.local |
http://localhost:4084 |
# Terminal 1 — from backend/
cp .env.example .env.local # fill AUTH_* (localhost:9000 Authentik)
make run
# Terminal 2 — from frontend/worksuite/
bun dev
Register Authentik URIs for split-port dev:
- OIDC callback (API):
http://localhost:5084/sign-in-oidc - Post-logout (Astro):
http://localhost:4084/sign-out
Via edge nginx: http://localhost:8084 (same-origin API + app).
Auth
OIDC BFF via shared Packages/authentication. Root .env.example for compose; backend/.env.example for host dev. Cookie name per tier: worksuite-auth-dev / -stage / -prod.
Full Authentik app setup: docs/SSO.md.
Go modules: no vendor/. Local dev uses go.work. Shared packages use replace in go.mod until Forgejo serves correct go-import meta tags.
Container build uses monorepo context (ArcaneDemesne/) — see backend/Dockerfile.
Migrations
One migration set for all tiers: backend/migrations/. Each tier has its own Postgres volume — run migrations after Postgres is up.
| Tier | Postgres (host) | Migrate |
|---|---|---|
| dev / local | 6084 | make migrate-up (backend/.env.local) |
| stage | 6184 | make migrate-up-stage |
| prod | 6284 | make migrate-up-prod |
make migrate-up # dev
make migrate-up-stage # after make up-stage
make migrate-up-prod # after make up-prod
make migrate-down # rollback one step (dev)
After recreating API/app containers, reload edge: make proxy-reload-dev, proxy-reload-stage, or proxy-reload-prod.
Forgejo stage deploy: .forgejo/workflows/deploy-stage.yaml.
DATABASE_URL must include timezone=UTC (see backend/.env.example).
Build
make build # Podman compose build
# From backend/
make -C backend build
# Workspace tests (backend + shared packages)
make test
Frontend routes
| Page | Path |
|---|---|
| Organizations | /organizations — list, members/invites modal, modal CRUD |
| All projects | /organizations/all/projects — projects across all your orgs |
| Projects | /organizations/{organizationId}/projects — list, edit modal, roster modal |
| All teams (global) | /organizations/all/projects/all/teams — teams across all orgs and projects |
| Org all teams | /organizations/{organizationId}/projects/all/teams — teams across projects in one org |
| Teams | /organizations/{organizationId}/projects/{projectId}/teams — project-scoped grid; .../teams/{teamId} deep-links team edit |
| Org teams shortcut | /organizations/{organizationId}/teams — redirects to org all teams |
| Invites | /invites/accept?token= — accept org invitation |
| Board | /organizations/{organizationId}/projects/{projectId}/board — Kanban |
| Org board shortcut | /organizations/{organizationId}/board — redirects to first project's board |
Reserved URL segment all (not a UUID) means aggregate scope in org/project filters. Header Projects and Teams default to the All views.
Creating an organization also seeds an example project and example team (names include org/project context) in the same transaction. Production: org create requires worksuite-admins when WORKSUITE_ALLOW_ORG_CREATE=false; join via invite accept.
Members, teams, and invites
| Flow | Rule |
|---|---|
| Join org | Invite accept (organization_invites.role → organization_members.role) |
| Project roster | Add org members to project first |
| Team roster | Pick from project members only; optional team membership |
| Remove from project | Cascades team membership for that project |
UI: Members on /organizations; Roster on projects grid + Manage roster in project edit modal; Teams in header nav with org/project filters and Members per team row.
API routes
| Resource | Path | Auth |
|---|---|---|
| Health | /api/v1/health |
Public |
| Profile | /api/v1/me/profile |
Session required |
| My organizations | GET /api/v1/organizations/mine |
Session required |
| My projects | GET /api/v1/organizations/mine/projects |
Session required |
| My teams | GET /api/v1/organizations/mine/teams |
Session required |
| Organizations | /api/v1/organizations |
Session (POST create); org member (read/update/delete) |
| Members | /api/v1/organizations/{organizationId}/members |
Org member |
| Invites | /api/v1/organizations/{organizationId}/invites |
Lead/Manager send; Lead/Manager revoke |
| Accept invite | POST /api/v1/invites/accept |
Session + matching email |
| My invites | GET /api/v1/invites/mine |
Session |
| Org all teams | GET /api/v1/organizations/{organizationId}/projects/teams |
Org member |
| Teams | /api/v1/organizations/{organizationId}/projects/{projectId}/teams |
Org member |
| Projects | /api/v1/organizations/{organizationId}/projects |
Org member |
| Features | /api/v1/organizations/{organizationId}/projects/{projectId}/features |
Org member |
| Work items | /api/v1/organizations/{organizationId}/projects/{projectId}/work-items |
Org member |
| Work item by number | GET .../work-items/by-number/{number} |
Org member |
| Tasks | /api/v1/organizations/{organizationId}/projects/{projectId}/work-items/{workItemId}/tasks |
Org member |
Org-scoped RBAC uses tier-based permissions in internal/authz/ (OWNER / MANAGER / EMPLOYEE).