Skip to content

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.

Start the isolated test course from the repository root:

Terminal window
scripts/test-course.sh up

The 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:

Terminal window
scripts/test-course-token.sh get jaakko

List every seeded account and role with:

Terminal window
scripts/test-course-token.sh users

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.

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:

Terminal window
scripts/test-course-token.sh verify student001
scripts/test-course-token.sh self-test jaakko

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.

Terminal window
cd aplus
./setup.sh

The 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.

Start from the repository template:

Terminal window
cp .env.example .env

Set at least these values in .env:

POSTGRES_PASSWORD=local-hanami-database-password
APLUS_BASE_URL=http://plus:8000/api/v2/
APLUS_URL=http://localhost:8000
ALLOW_PRIVATE_COURSE_CONFIG_URL=true
SMTP_HOST=mailpit
SMTP_PORT=1025

Generate the independent frontend credentials and append them to .env:

Terminal window
printf 'FRONTEND_SESSION_ENCRYPTION_KEY=%s\nFRONTEND_SERVICE_SECRET=%s\n' \
"$(openssl rand -base64 32)" "$(openssl rand -base64 32)" >> .env

The private-URL opt-out lets Hanami fetch the sample course configuration from the Compose network. Use it only for this disposable local stack.

Terminal window
docker compose --profile aplus up --build

The 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.

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.json

If the backend runs natively, use the host form instead:

http://localhost:8080/static/default/_static/hanami_course_config.json

The course static files come from the grader on port 8080. The A+ front on port 8000 serves only A+’s own static assets.

Create a token for each role you want to test:

Terminal window
docker compose exec plus run-django.sh manage drf_create_token teacher
docker compose exec plus run-django.sh manage drf_create_token assistant
docker compose exec plus run-django.sh manage drf_create_token student

Use 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.

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:

Terminal window
docker compose exec -T plus run-django.sh manage shell <<'PY'
from external_services.models import LTIService, MenuItem
from 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},
)
PY

Add the matching local-only values to .env and restart the frontend:

LTI_CONSUMER_KEY=hanami
LTI_CONSUMER_SECRET=hanami-secret
LTI_ALLOW_WEAK_SECRET=true
Terminal window
docker compose up -d frontend

Create 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.

  • The grader mounts /var/run/docker.sock because it starts grading containers. Do not expose this local stack to untrusted users.
  • Give parallel stacks different COMPOSE_NETWORK_NAME and GRADER_RUNNER_NAMESPACE values 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 down keeps the shared volumes. Add -v only when you intend to delete the local Postgres and A+ data.