- Go 99.6%
- Shell 0.2%
- Makefile 0.1%
Spec previously declared emptyObj on listTimetables, listEntries, all four schedule endpoints and bulkCreateEntries; handlers actually emit envelope-wrapped shapes. Adds TimetableList / EntryList / BulkEntriesResult and four typed schedule envelopes (ClassSchedule / PeriodSchedule / TeacherSchedule / SchoolDaySchedule). Also fixes the schoolDaySchedule signature — it takes ?day=N (required) and optional ?at=YYYY-MM-DD, not the ?week= the spec had documented. Pulled into its own helper since the shape differs from the week views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| deploy/k8s | ||
| docs | ||
| libs | ||
| services | ||
| tools | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.work | ||
| go.work.sum | ||
| Makefile | ||
| README.md | ||
School/Daycare/University SaaS Ecosystem
Multi-tenant SaaS for schools, universities, and daycares. Hosted by Synthrik. Currently unnamed at the product level — see CLAUDE.md for context.
Documentation
| File | Purpose |
|---|---|
CLAUDE.md |
Project context, conventions, and gotchas — read this first |
docs/ARCHITECTURE.md |
Design narrative, key decisions, rationale |
docs/CONVENTIONS.md |
Coding patterns to follow |
<service>/v1/openapi.json + /docs |
Per-service OpenAPI 3.1 spec + Swagger UI — replaces the old hand-maintained endpoint catalogue |
docs/ROADMAP.md |
What's done, what's next, what we explicitly rejected |
docs/BACKLOG.md |
Cross-service feature menu — pick from this for module-deepening |
Per-service README.md |
Tables, REST surface, and run instructions per service |
Architecture in one paragraph
Go microservices. Each service is its own Go module under services/, with its own migrations/, port, AI manifest, and permission manifest. Shared plumbing lives in libs/. PostgreSQL with one schema per service. Identity signs RS256 JWTs; every other service verifies with the public key only — no shared secrets. Schools subscribe to modules from catalog; the entitlements middleware gates bolt-on routes against the JWT's entitlements claim, and per-handler Principal.HasPerm(schoolID, "<service>:<resource>.<verb>") checks gate fine-grained operations against the JWT's perms map. Identity polls every service's /v1/permissions to keep the central role/permission registry hot. The ai-gateway polls every service's /v1/aiport and exposes the AI-safe operations as REST tools and via MCP JSON-RPC.
Hierarchy
organization
└── school (institution_type: school | university | daycare)
├── school_group (optional middle layer for trusts/districts)
├── family ── student
│ ── guardian
├── staff
├── class
└── (bolt-on data: attendance, library, ...)
Repo layout
synthrik/
├── CLAUDE.md, README.md ← project front door
├── docs/ ← architecture, conventions, roadmap
├── libs/ ← shared Go packages (no domain logic)
│ ├── httpkit/, authctx/, pgkit/, entitlements/
│ └── events/, obs/, apispec/
├── services/ ← one Go module per service
│ ├── identity/, tenancy/, directory/, catalog/, billing/ (foundation)
│ ├── attendance/, library/, comms/ (bolt-on)
│ ├── timetables/, signage/ (placeholder bolt-ons)
│ └── ai-gateway/ (platform: aggregator + MCP)
├── tools/ ← gen-jwt-keys.sh and friends
├── deploy/k8s/ ← Kustomize manifests for the school namespace
├── .forgejo/workflows/ ← Forgejo Actions: build → push → kubectl apply
├── Dockerfile ← multi-stage, keyed by SERVICE build arg
├── go.work ← lists all 16 modules
├── Makefile, docker-compose.yml, .gitignore
└── .keys/ ← dev RSA keys (gitignored)
Services
| Service | Type | Default port | Purpose |
|---|---|---|---|
identity |
foundation | 8001 | Users, sessions, JWT issuance, device pairing, role grants |
tenancy |
foundation | 8002 | Organizations, school groups, schools |
directory |
foundation | 8003 | Families, students, guardians, staff, classes |
catalog |
foundation | 8004 | Module SKUs and price list (ZAR-first, multi-currency-ready) |
billing |
foundation | 8005 | Subscriptions, invoices, payments, entitlements feed for identity |
attendance |
bolt-on (reference) | 8010 | Attendance sessions and records |
library |
bolt-on (reference) | 8011 | Books, per-physical-copy rows, loans, fines, reading-history & popularity analytics |
timetables |
bolt-on | 8012 | Per-school weekly schedules: timetables (draft/active/archived) + entries (day_of_week × period × class), per-class / per-period / per-teacher week views |
signage |
placeholder | 8013 | Kiosks, classroom door displays, wayfinding |
comms |
bolt-on | 8014 | Per-school SMTP config; will route platform events to email (SMS / Telegram next) |
gradebook |
bolt-on | 8015 | Curriculum-agnostic assessment + reporting (academics SKU). Scales (numeric / coded / qualitative / narrative / pass_fail), subjects (with secular / religious / vocational / special / extramural tracks), per-class subject offerings, and a preset catalogue covering CAPS / IEB / Cambridge / IB / Montessori / ACE / NCV / Hifz. |
admissions |
bolt-on | 8016 | Online enrolment (admissions SKU). Per-school intakes (state machine: draft / open / closed / lottery / admitting / completed / cancelled), public unauth application form, encrypted-at-rest applicant identifications, deterministic lottery, atomic admit into directory (family + student + guardians + identifications). |
storage |
platform | 8017 | Bundled object storage (MinIO-backed). Owning services delegate uploads + downloads via shared-bearer internal endpoints; public download path mints HMAC-signed short-lived URLs. v1 used by admissions's applicant document uploads; gradebook artefacts adopt later. |
ai-gateway |
platform | 8020 | Aggregates /v1/aiport manifests; REST + MCP for AI clients |
Quick start
make keys # one-time: generate RSA key pair under .keys/
make up # postgres + nats via docker compose
make tidy # go mod tidy across all 16 modules
make migrate # run goose migrations for every service that has them
make run/identity # run a service in the foreground
Bootstrap the foundation (no manual SQL needed)
# 1. Create the org (unauth bootstrap, gate at ingress in production)
ORG=$(curl -s -X POST localhost:8002/v1/organizations \
-d '{"slug":"acme","name":"Acme Schools"}' | jq -r .id)
# 2. Register the first user — auto-granted org_admin because they're the first
USER=$(curl -s -X POST localhost:8001/v1/users \
-d "{\"organization_id\":\"$ORG\",\"email\":\"admin@acme.test\",\"password\":\"correct horse battery staple\",\"display_name\":\"Admin\"}" | jq -r .id)
# 3. Login → JWT carries roles: ["org_admin"]
TOKEN=$(curl -s -X POST localhost:8001/v1/sessions \
-d '{"email":"admin@acme.test","password":"correct horse battery staple"}' | jq -r .access_token)
# 4. Create the first school (org_admin gated)
SCHOOL=$(curl -s -X POST localhost:8002/v1/organizations/$ORG/schools \
-H "Authorization: Bearer $TOKEN" \
-d '{"slug":"riverside","name":"Riverside High","institution_type":"school"}' | jq -r .id)
# 5. Grant self a school role (so the JWT carries school_ids on next login)
curl -s -X POST localhost:8001/v1/users/$USER/school-roles \
-H "Authorization: Bearer $TOKEN" \
-d "{\"school_id\":\"$SCHOOL\",\"role\":\"teacher\"}"
# 6. Re-login to refresh JWT with school_ids
TOKEN=$(curl -s -X POST localhost:8001/v1/sessions \
-d '{"email":"admin@acme.test","password":"correct horse battery staple"}' | jq -r .access_token)
# 7. Subscribe the school to attendance + library
for sku in attendance library; do
curl -s -X POST localhost:8005/v1/schools/$SCHOOL/subscriptions \
-H "Authorization: Bearer $TOKEN" \
-d "{\"sku\":\"$sku\"}"
done
# 8. Re-login one more time so JWT entitlements include the new SKUs
TOKEN=$(curl -s -X POST localhost:8001/v1/sessions \
-d '{"email":"admin@acme.test","password":"correct horse battery staple"}' | jq -r .access_token)
The smoke-test sections in services/attendance/README.md and services/library/README.md continue from here.
AI surface
Every bolt-on declares an AI manifest at GET /v1/aiport listing the operations safe for AI clients. The ai-gateway service polls these manifests every 5 minutes (or on POST /v1/admin/refresh), exposes them as a REST tool catalogue at /v1/tools, and as MCP JSON-RPC at /mcp. The user's JWT is forwarded verbatim to upstream — the gateway adds zero authority.
# What can AI agents do today?
curl -s localhost:8020/v1/tools | jq '.tools[] | "\(.id) — \(.summary)"'
Conventions in 30 seconds
- Tables prefixed with the owning service:
library_books,attendance_sessions,identity_users. - Money is
amount_cents BIGINT+currency CHAR(3), defaultZAR. Never floats. - Cross-service references are by ID, not FK-enforced.
- Auth checks (
BelongsToSchool,OrganizationID == path.org_id) are inline per handler — uniform, greppable, impossible to skip silently. - Errors are RFC 7807-ish via
httpkit.{BadRequest, NotFound, Forbidden, ...}. - AI safety is opt-in per operation. Mutations stay
AISafe: false. - RBAC: each service declares permissions at
/v1/permissions; identity caches them, schools compose roles, JWTs carry a per-schoolpermsmap.org_adminis the universal escape hatch. - Date ranges are
?from=YYYY-MM-DD&to=YYYY-MM-DD, both inclusive (server bumpstoby a day, SQL uses< to).
The full conventions list is in docs/CONVENTIONS.md.