AetherCode is an open-source coding-assessment platform for colleges: staff write programming questions with hidden tests, students sit timed exams in a HackerRank-style editor (optionally locked down in Safe Exam Browser), and submissions are compiled and judged in a sandboxed code-execution engine.
It is built for supervised college lab exams and released under the GNU AGPL v3.
The repository holds two independent codebases:
| Platform | Exam app v1 | |
|---|---|---|
| Path | backend/, deploy/, frontend/ |
apps/exam-v1/ |
| What it is | Multi-tenant Go microservices with PostgreSQL row-level security, signed authorization capabilities, an isolated judge (Piston or Judge0) and Safe Exam Browser enforcement. Runs on one server with Docker Compose or on Kubernetes. | One Next.js app + PostgreSQL + a grading worker over Judge0 or Piston, deployed with Docker Compose behind nginx on one server. |
| Status | The only backend going forward (ADR-0017). Accounts, authoring, take-and-grade and SEB lockdown work end to end through the gateway; results, live operations and the web frontend are next. See the roadmap. | Frozen fallback, feature-complete for a supervised lab exam; used for exams until the platform reaches parity, then removed. |
| Start here | PLAN.md, docs/roadmap.md, deploy/single-server | apps/exam-v1/README.md |
The split, and why it exists, is recorded in ADR-0016 and ADR-0017. The two share no code or database.
Everything a college needs to run a graded coding exam in a supervised lab:
- Admin:
- CSV student import with generated passwords and printable login slips;
- batch password reissue, faculty accounts, enable/disable;
- a system status page (database, worker, engine, grading queue).
- Faculty:
- questions with sample and hidden tests (typed, or imported from files or a HackerRank-style zip);
- exams per batch with a time window and per-student duration;
- question pools (each student draws one per slot);
- lab lockdown: allowed networks, fullscreen gate, outside-paste blocking;
- a live monitor with announcements, per-student extra time and regrading;
- CSV export, result release, and a code-similarity report.
- Students:
- a HackerRank-style exam screen in C, C++, Java or Python;
- Run against samples, Submit against all tests, custom input;
- per-test results, with hidden tests shown as pass/fail only;
- autosave, a server-owned timer, and auto-submission at time-up.
- Grading:
- a worker that compiles once and runs every test in one Judge0 job;
- Piston and per-test fallbacks;
- retries, crash recovery, and runs prioritised over submits.
- Operations:
- Docker Compose with nginx, Postgres, Judge0, and 15-minute backups;
pnpm engine-checkandpnpm loadtest.
make exam-check # typecheck + unit tests
make exam-build # production build
make exam-up # full deployment (configure apps/exam-v1/deploy first)Before the first graded exam on a new server:
- Run
pnpm engine-checkinside the worker container. It must print "Engine OK". - Run a load test at the real student count.
- Hold a mock exam in the lab.
Judge0 1.13 needs cgroup v1 on the host. The full deployment guide and every rule (scoring, timing, visibility, limits) are in the app README.
backend/services/: independently deployable Go services (gateway, identity, tenant, user, question-bank, assessment, submission, judge, seb, notification, analytics).backend/libs/pkg/: shared, framework-neutral platform packages.backend/libs/proto/: the source of truth for internal gRPC contracts.deploy/: the single-server Docker Compose stack, Helm charts and database provisioning.docs/: architecture decision records, database documentation, runbooks, API output and the roadmap.frontend/: reserved for the platform's Next.js frontend (not started).
Start with the implementation plan, the documentation index and the roadmap, which also lists the production gates that need infrastructure outside this repository.
The User service is the canonical Casbin-backed authorization decision service.
Its private mTLS authz/v1.Authorize API issues a fresh, five-second,
database-audience-bound HMAC capability for each allowed request. A target
database validates that capability in authz.set_context, binds it to the
current PostgreSQL backend and transaction, and checks the local
authorization-revision projection under FORCE ROW LEVEL SECURITY. A failed
decision, expired capability, or projection lag denies access. The complete
contract is in docs/database/authorization-context.md.
Authorization recovery is fail closed as well. Each RLS-protected service starts with its local authorization projection unavailable and, after an outbox or authorization-consumer failure, writes a target-specific resync request through its local outbox. The User service returns a manifest-verified grant snapshot; the target reopens only after every item has been applied. This prevents a stream-retention gap from silently retaining access after a revocation.
Every stateful platform service owns one logical database in the three-node
platform PostgreSQL topology. Database owners are non-login roles; migrations,
applications, and authorization-projection workers use separate least-privilege
identities. Run migrations only as the service migrator after the role/database
provisioner has run. Authorization HMAC material is supplied by the approved
KMS/secret controller after bootstrap with
backend/scripts/provision-authz-context-key;
the script neither generates nor stores a secret.
The platform HA chart is deliberately render-gated on client certificates and India-resident encrypted backup inputs. A successful render or install is not HA acceptance: the node-failure and PITR exercises in the platform PostgreSQL runbook are required before promotion.
Judge control-plane state is isolated in aether_judge_wrapper with its own
PostgreSQL HA deployment and RabbitMQ quorum cluster. It does not own or share
Redis with the platform; Redis is an internal dependency of the separately
operated Judge0 engine after approval. The wrapper accepts durable, encrypted
references and leases completions through private mTLS gRPC.
Implemented on the judge side:
- a real Judge0 HTTP client (
backend/services/judge/internal/adapters/judge0), selected withJUDGE_ENGINEand only enabled whenJUDGE_ENGINE_COMPATIBILITY_APPROVEDis set; - fan-out of an evaluation bundle into one execution unit per test case (ADR-0014);
- per-unit results surfaced to submission, with a candidate-versus-faculty visibility boundary (ADR-0015).
Submission dispatches each queued evaluation request and code run to Judge over mTLS; Judge decrypts the bundle and source, runs one execution unit per test case on the engine, and Submission scores the attempt by test weight (ADR-0014, ADR-0015, ADR-0021). The single-server stack uses Piston by default (ADR-0018).
The Judge0 engine chart is disabled by default. It must remain blocked until the gVisor, no-network, non-privileged compatibility gate has approved an immutable image and recorded queue-replay, node-failure, and 10,000-candidate / five-minute load evidence, including the 60-second final-verdict P95 target. See the compatibility-gate runbook.
Working end to end through the gateway (verified by
deploy/single-server/smoke.py):
- identity: login by username or roll number, MFA and recovery; administrator-provisioned accounts and CSV student import (ADR-0019);
- colleges, departments, batches, roles and placement affiliations;
- staff authoring into a global question bank with server-built, encrypted, weighted test bundles (ADR-0020); immutable question and exam versions; batch assignment;
- attempts with per-candidate deadlines, autosaved answers, Run against sample tests with full output (ADR-0021), submit, judging, weighted scoring and time-up auto-submission;
- Safe Exam Browser lockdown with per-URL key checks and
.seblaunch files (ADR-0022); - in-app notifications, event-fed analytics projections, cursor-paginated lists and soft delete (ADR-0013).
The local MinIO and KMS adapters (backend/libs/pkg/storage/minio,
backend/libs/pkg/kms/local) are what the single-server stack uses. A
multi-college production deployment should use managed object storage and
KMS; see the production gates in the roadmap.
A fresh deployment has no principals and no role assignments, so there is no way to call any authenticated endpoint. Run the one-time bootstrap command to create the first platform administrator after migrations are applied:
export IDENTITY_DATABASE_URL="postgres://..."
export USER_DATABASE_URL="postgres://..."
make bootstrap EMAIL=admin@college.edu NAME="Platform Admin"The command creates the principal in the identity database and a self-granted
super_admin role assignment in the user database. The created account has no
password. Activate it by triggering the password-reset flow for the supplied
email address. No password is ever written, printed, or accepted by this
command.
The command is safe to re-run. A crash between the two database writes is
repaired by running it again — each half independently no-ops if its row already
exists. Once the platform has any principal or any super_admin, both functions
permanently refuse further calls.
authz.context_keys (present in the analytics, assessment, identity,
notification, question-bank, seb, submission, tenant, and user
databases — not gateway or judge, which have no such table) supports
zero-downtime rotation through overlapping not_before/not_after/retired_at
validity windows, but publishing and retiring a key still requires an operator
action. make rotate-authz-key targets exactly one database per invocation:
export DATABASE_URL="postgres://..."
make rotate-authz-key ACTION=publish AUDIENCE=aether_submission NOT_AFTER=2026-09-24T00:00:00ZThe command generates a new key ID (a UUIDv7, unless KEY_ID is supplied) and
32 bytes of random HMAC key material, inserts the row, and prints the key ID
and base64-encoded secret to stderr exactly once, with a clear warning that
it is the only time the secret is shown. The secret is never written to a file,
a log, or stdout. Copy it out-of-band into the target's operational secret
store before it is lost.
Full rotation procedure:
- Publish a new key against all nine target databases, one invocation per
database, with a
not-beforea few minutes in the future (the default) so already-deployed services have time to pick up the new configuration before the key becomes valid, and anot-afterfar enough out to cover the rotation window. - Wait until
not-beforehas passed on every database, then confirm the new key works before relying on it. - Update
AUTHZ_CAPABILITY_KEYSin the User service's configuration (the canonical signing service, perbackend/libs/pkg/authz) to the new key. - Once confident no capability signed with the old key is still in flight —
capabilities have a five-second TTL (
capabilityTTLinbackend/libs/pkg/authz/capability.go), so the safe window is generous — retire the old key on all nine databases:
export DATABASE_URL="postgres://..."
make rotate-authz-key ACTION=retire AUDIENCE=aether_submission KEY_ID=<old-key-id>retire fails if the key is already retired or does not exist for that
audience. This CLI is for operational key rotation; the KMS-provisioned
first key for a freshly bootstrapped database still comes from
backend/scripts/provision-authz-context-key,
whose secret is supplied externally rather than generated locally.
- Exam app: Node.js 24, pnpm 12, Docker. See apps/exam-v1/README.md.
- Platform: Go, Docker, GNU Make, Buf, and golangci-lint. The pinned
golang-migraterunner is built through Go, so no separately installed migration CLI is needed. Copy.env.exampleto.envand replace development passwords before running the local stack.
# exam app
make exam-check
make exam-build
make exam-up # / make exam-down
# platform
make dev-up
make dev-judge-up
make build
make test
make test-migrations
make lint
make migrate SVC=identity DIR=upmake dev-up starts the platform compose profile. make dev-judge-up starts
the isolated Judge control-plane profile using an untracked .judge-control.env
file; it intentionally does not start Judge0. make test-integration requires
Docker. Production credentials and database roles are provisioned through the
deployment configuration, never from this repository. The full command list is
in CLAUDE.md.
Contributions are welcome: read CONTRIBUTING.md first, and follow the Code of Conduct. Report security issues privately as described in SECURITY.md, never in a public issue.
Copyright (C) 2026 St. Joseph's Group of Institutions.
AetherCode is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License v3.0. If you run a modified version as a network service, the AGPL requires you to offer its users the corresponding source code. See NOTICE.
