No description
  • Go 99.6%
  • Shell 0.2%
  • Makefile 0.1%
Find a file
Wesley Channon 83ca4204ec
All checks were successful
deploy / detect (push) Successful in 4s
deploy / verify (push) Successful in 4m34s
deploy / build (platform-docs) (push) Successful in 24s
deploy / build (timetables) (push) Successful in 26s
deploy / deploy (push) Successful in 7s
api: type timetables list + schedule + bulk responses
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>
2026-05-14 09:51:51 +02:00
.forgejo/workflows ci: force rebuild_all — recover from /api migration detect miss 2026-05-11 19:59:30 +00:00
deploy/k8s deploy: wire tenancy internal token + give directory a tenancy client 2026-05-12 07:35:57 +00:00
docs docs: ROADMAP entry for Phase 83 (audit closeout) 2026-05-11 14:53:45 +00:00
libs sec: LOW polish batch — err leaks / Content-Disposition / SoftMiddleware doc / k8s defaultMode (Phase 83) 2026-05-11 13:54:52 +00:00
services api: type timetables list + schedule + bulk responses 2026-05-14 09:51:51 +02:00
tools docs: fix stale path references in merged spec description 2026-05-11 19:57:01 +00:00
.dockerignore deploy: containerise + k8s manifests + Forgejo Actions CI/CD 2026-05-01 06:22:27 +02:00
.gitignore platform-docs: serve merged OpenAPI + Swagger UI at api.school.synthrik.com/ (Phase 78) 2026-05-10 19:41:36 +02:00
CLAUDE.md api: backend services register at /api/v1/<service>/<resource> natively 2026-05-11 19:56:01 +00:00
docker-compose.yml Initial commit 2026-04-30 21:05:40 +02:00
Dockerfile ci+build: narrow per-service COPY and disable compressed kaniko caching 2026-05-01 09:59:11 +02:00
go.work platform-docs: serve merged OpenAPI + Swagger UI at api.school.synthrik.com/ (Phase 78) 2026-05-10 19:41:36 +02:00
go.work.sum ingress: api.school.synthrik.com path-prefix routing + go.sum repair 2026-05-10 19:05:55 +02:00
Makefile platform-docs: serve merged OpenAPI + Swagger UI at api.school.synthrik.com/ (Phase 78) 2026-05-10 19:41:36 +02:00
README.md docs: retire ENDPOINTS.md (superseded by per-service OpenAPI specs) 2026-05-10 18:05:59 +02:00

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), default ZAR. 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-school perms map. org_admin is the universal escape hatch.
  • Date ranges are ?from=YYYY-MM-DD&to=YYYY-MM-DD, both inclusive (server bumps to by a day, SQL uses < to).

The full conventions list is in docs/CONVENTIONS.md.