Local A+
Hanami includes two local A+ workflows. Both use disposable accounts and data.
Use the comprehensive course for product QA with realistic grading, feedback, email, LTI, templates, and manual grading. It runs Hanami on port 8181, the documentation edge on port 8182, and A+ on port 8100.
Use the main Compose aplus profile for a smaller environment where you create
data by hand and keep using the normal .env file. It runs Hanami on port 3000
and A+ on port 8000.
Comprehensive test course
Section titled “Comprehensive test course”Start the isolated test course from the repository root:
scripts/test-course.sh upThe script generates the course, starts Hanami and A+, and creates deterministic fixture data when needed. Missing course assets trigger an upstream A+ manual checkout, course compilation, and grader image builds. Existing assets and fixture volumes are reused after an A+ integrity check.
Use verify for the complete cross-service check, benchmark for browser page
timings and UI workflows, seed for fresh fixture data, or reset to test
setup with empty volumes. The script uses its own environment, Compose project,
and volumes.
When the command finishes, open:
| Service | URL | Use |
|---|---|---|
| Hanami | http://localhost:8181 | Grading, feedback, course settings, and student views |
| Docs | http://localhost:8182 | Starlight, Kotlin reference, and public OpenAPI |
| A+ | http://localhost:8100 | Accounts, submissions, grades, and the Hanami LTI menu item |
| Mailpit | http://localhost:8125 | Feedback email sent by the fixtures or UI |
The documentation edge serves the Kotlin API reference, a
static OpenAPI reference, and the underlying public
OpenAPI document at /reference/openapi/public-v1.json. The internal frontend
specification is not published.
Local account passwords match their usernames. Useful accounts include:
| Username | Role |
|---|---|
teacher |
Course teacher |
jaakko |
Teacher and configured grader |
andy |
Assistant with language and tag assignments |
student001 |
Student with seeded submissions and feedback |
Log in to A+ and open Hanami from the course menu to test the full LTI flow. You can also get a token for the manual Hanami sign-in gate:
scripts/test-course-token.sh get jaakkoList every seeded account and role with:
scripts/test-course-token.sh usersWhat is seeded
Section titled “What is seeded”The course contains 100 students and ten graders across English, Finnish, and Swedish tag assignments. Its assignments cover Python, Scala 3, mixed-language projects, group submissions, multiple revisions, long files, and files that partly match course templates.
The seed also creates rubrics, grader assignments, draft and ready assessments, sent grading examples, SARIF-backed feedback documents, rendered email, and 90 pending manual ZIP capstones. The grading and feedback views work without manual setup or submissions.
Manage the isolated stack
Section titled “Manage the isolated stack”| Command | Effect |
|---|---|
scripts/test-course.sh verify |
Verify generated fixtures and live A+/Hanami state. |
scripts/test-course.sh benchmark |
Measure every frontend page and run repeatable browser workflows. |
scripts/test-course.sh seed |
Clear fixture-owned Hanami data and seed it again without rebuilding the stack. |
scripts/test-course.sh verify-live |
Create, grade, verify, and remove one deferred manual-grading submission. |
scripts/test-course.sh down |
Stop the isolated containers and keep their volumes. |
scripts/test-course.sh reset |
Delete the isolated volumes, rebuild, seed, and verify from scratch. |
verify-live changes only one disposable capstone submission and removes it
after the check. The 90 seeded pending capstones remain untouched.
benchmark records cold and warm navigation data, TTFB, FCP, LCP, CLS, sampled
INP, long tasks, transfer size, and browser diagnostics at
reports/test-course-browser-benchmark.json. It compares grading navigation
with and without SvelteKit’s intent preloading, signs in through the real A+
LTI consent page, sends one feedback email to Mailpit, and removes its temporary
teacher and student sessions when the run finishes.
Token rotation and revocation tests are available through
scripts/test-course-token.sh:
scripts/test-course-token.sh verify student001scripts/test-course-token.sh self-test jaakkoShared local development stack
Section titled “Shared local development stack”The main Compose project can start a smaller A+ sample course beside your normal Hanami development services.
Use this stack for day-to-day development when frontend hot reload and manually
managed course data are more useful than deterministic fixtures. The
comprehensive course remains the preferred environment for product and release
QA. For a frontend-only change, running bun dev through
scripts/dev-frontend.sh is faster than either Compose stack.
1. Build the sample course
Section titled “1. Build the sample course”cd aplus./setup.shThe setup script clones apluslms/aplus-manual with its submodules into the
git-ignored aplus/aplus-manual/ directory, installs Hanami’s sample fixtures,
and compiles the course. Run it again after updating the upstream checkout or
the tracked fixtures.
2. Configure .env
Section titled “2. Configure .env”Start from the repository template:
cp .env.example .envSet at least these values in .env:
POSTGRES_PASSWORD=local-hanami-database-passwordAPLUS_BASE_URL=http://plus:8000/api/v2/APLUS_URL=http://localhost:8000ALLOW_PRIVATE_COURSE_CONFIG_URL=trueSMTP_HOST=mailpitSMTP_PORT=1025Generate the independent frontend credentials and append them to .env:
printf 'FRONTEND_SESSION_ENCRYPTION_KEY=%s\nFRONTEND_SERVICE_SECRET=%s\n' \ "$(openssl rand -base64 32)" "$(openssl rand -base64 32)" >> .envThe private-URL opt-out lets Hanami fetch the sample course configuration from the Compose network. Use it only for this disposable local stack.
3. Start Hanami and A+
Section titled “3. Start Hanami and A+”docker compose --profile aplus up --buildThe profile adds these services:
| Service | Default URL |
|---|---|
| A+ | http://localhost:8000 |
| Grader | http://localhost:8080 |
| Mailpit | http://localhost:8025 |
The Compose file pins the A+ frontend and grader images to immutable manifests.
The sample course is Def. Course, with course id 1. Pre-seeded usernames
include teacher, assistant, student, student1 through student500, and
the Django superuser root. Each password matches the username.
4. Create a course configuration
Section titled “4. Create a course configuration”Hanami starts with no configured courses. Sign in as teacher, open Hanami,
and choose New course. Create the Def. Course configuration, then set the
course structure URL under Settings and Integration:
http://grader:8080/static/default/_static/hanami_course_config.jsonIf the backend runs natively, use the host form instead:
http://localhost:8080/static/default/_static/hanami_course_config.jsonThe course static files come from the grader on port 8080. The A+ front on port 8000 serves only A+’s own static assets.
5. Create API tokens
Section titled “5. Create API tokens”Create a token for each role you want to test:
docker compose exec plus run-django.sh manage drf_create_token teacherdocker compose exec plus run-django.sh manage drf_create_token assistantdocker compose exec plus run-django.sh manage drf_create_token studentUse the printed token at Hanami’s sign-in gate. The run-django.sh manage
wrapper is required because a bare manage.py command cannot reach the A+
container’s database.
Test LTI sign-in
Section titled “Test LTI sign-in”The comprehensive course already includes a working LTI menu item. To add one to the shared sample course, first seed the service and course menu item:
docker compose exec -T plus run-django.sh manage shell <<'PY'from external_services.models import LTIService, MenuItemfrom course.models import CourseInstance
service, _ = LTIService.objects.update_or_create( consumer_key="hanami", defaults={ "menu_label": "Hanami", "menu_icon_class": "postcard-heart", "url": "http://localhost:3000/lti/launch", "consumer_secret": "hanami-secret", "access_settings": 10, "destination_region": 0, "enabled": True, },)course = CourseInstance.objects.get(id=1)MenuItem.objects.update_or_create( course_instance=course, service=service, defaults={"access": 0, "menu_icon_class": "postcard-heart", "enabled": True},)PYAdd the matching local-only values to .env and restart the frontend:
LTI_CONSUMER_KEY=hanamiLTI_CONSUMER_SECRET=hanami-secretLTI_ALLOW_WEAK_SECRET=truedocker compose up -d frontendCreate an API token for the A+ user before launching. Then log in to A+, open the course’s Hanami menu item, and accept the one-time consent page. See Sign-in and LTI for the production setup and troubleshooting steps.
Local environment caveats
Section titled “Local environment caveats”- The grader mounts
/var/run/docker.sockbecause it starts grading containers. Do not expose this local stack to untrusted users. - Give parallel stacks different
COMPOSE_NETWORK_NAMEandGRADER_RUNNER_NAMESPACEvalues so their grader containers and staged files do not collide. - Use only disposable tokens and secrets. Remove any real credential committed to fixtures or shell history.
- Runtime fixture manifests contain identifiers and deterministic state, not A+ tokens. Provisioning receives disposable tokens only through its process environment and accepts non-loopback Hanami origins only through an explicit exact-origin allowlist.
docker compose downkeeps the shared volumes. Add-vonly when you intend to delete the local Postgres and A+ data.