Architecture
Hanami has three application boundaries:
- The browser talks to SvelteKit through server loads and SvelteKit remote functions.
- SvelteKit calls Ktor through the private
/internal/frontendAPI. - Ktor calls A+ through four feature-owned ports for identity, courses, submissions, and grade publication.
The browser stores a random Hanami session identifier in an HttpOnly cookie. Ktor stores only its SHA-256 digest and encrypts the corresponding A+ token with AES-256-GCM. SvelteKit never returns the A+ token to the browser after sign-in.
Frontend
Section titled “Frontend”Frontend code is grouped by product feature under frontend/src/lib. A feature can contain browser API entry points, owned models, server adapters, and reusable UI. Routes parse URL parameters, load navigation-critical data, and compose pages.
Remote functions are the browser data API. They validate their input with Valibot and call a feature server service. Initial route data still comes from parallel server loads when the page needs it before rendering.
Hanami follows the ownership model in the SvelteKit remote-functions documentation. query reads dynamic data, query.batch combines related lookups, command handles programmatic mutations, and form is for progressively enhanced forms. Each input has a Standard Schema validator. Mutations update the affected queries instead of invalidating all page data. Remote functions and component await are experimental, so dependency updates require the complete frontend gate and a review of the generated endpoints.
Navigation-critical requests stay in parallel server loads. Component-level await is limited to independent regions with a nearby <svelte:boundary> for pending and failed states; the Svelte await documentation explains the rendering and synchronization semantics. This prevents nested component work from turning route loading into a request waterfall.
Request state is created per request or per component tree. Server loads do not mutate shared module state, following SvelteKit state management. Secrets, the generated Ktor client, and feature server adapters stay in $lib/server or .server modules so SvelteKit’s server-only module checks reject client imports during development and builds. Effects that install observers, timers, or event handlers return cleanup functions according to the Svelte lifecycle contract.
The server hook resolves the opaque session into request-local identity and preferences, following SvelteKit’s authentication guidance. Route loads read that state from event.locals; they do not repeat authentication or share mutable identity state. The handleError hooks follow SvelteKit’s error-hook contract: logs retain the full exception and request ID, while browser error objects contain only safe fields.
Ktor generates backend/openapi/frontend.json from the /internal/frontend route tree and backend/openapi/public-v1.json for the public automation API. The specifications have independent authentication and contracts. openapi-typescript generates the server-only internal transport types. Feature adapters parse responses again with Valibot before returning frontend models. Components and remote functions do not import generated DTOs.
Each eager JavaScript chunk has a 500 KiB raw limit. CI checks per-route byte budgets and keeps Shiki on the server. It also rejects an unbounded Highlight.js grammar set and scans the built client for credentials, private Ktor routes, and server-only environment modules.
Route data and code preloading use SvelteKit’s documented link options. Eager preloading is limited to likely destinations, and route loads stay parallel as recommended by the SvelteKit performance guidance. forkPreloads stays disabled until repeatable production-browser measurements show at least a 10% improvement in median warm course navigation. The measurement must show no extra backend calls, hydration warnings, mutations, or focus changes. The flag is experimental and can change outside semantic-version guarantees, as described in the SvelteKit configuration reference.
Backend
Section titled “Backend”The backend is one Gradle module organized by feature. Application.kt creates the service graph explicitly. Route groups receive an authorizer and the feature service they call. Services own workflows, and repositories own database transactions.
All A+ wire DTOs and authenticated transport code live under aplus. Feature code sees Hanami models through these ports:
IdentitySourceCourseSourceSubmissionSourceGradePublisher
Every course request resolves the configured course and the caller’s current role before a feature cache is read. Student submission access also checks submitter ownership.
Persistence
Section titled “Persistence”Postgres stores course settings, feedback documents, rubrics, assessments, grading assignments, manual-grading state, user preferences, and frontend sessions. DatabaseSchema.initialize creates the complete schema. Development databases are reset by deleting their Postgres volume. scripts/test-course.sh reset recreates the isolated course database and deterministic fixtures.
A+ remains the system of record for course membership, submissions, and published grades. Hanami stores A+ user IDs instead of copied names or email addresses and resolves current profiles when needed.
HTTP boundaries
Section titled “HTTP boundaries”| Boundary | Purpose | Authentication |
|---|---|---|
/internal/frontend |
SvelteKit to Ktor | Frontend service secret; authenticated operations also require an opaque Hanami session |
/api/v1 |
Supported course-settings automation API | A+ token |
/api/internal/manual-grading |
Manual-grader intake and fixture tooling | Manual-grading registration secret |
/health/* and /metrics |
Deployment health and metrics | Private deployment network |
Internal frontend errors use code, optional safe detail, requestId, and optional fieldErrors. Ktor preserves useful status codes and revision headers without sending A+ bodies, upstream URLs, SQL errors, SMTP diagnostics, tokens, or service credentials.
Source map
Section titled “Source map”| Path | Responsibility |
|---|---|
app/ |
Validated configuration and lifecycle settings |
auth/ |
Identities, roles, token cache, and frontend sessions |
concurrency/ |
Coalescing, concurrent mapping, cache pruning, and scopes |
content/ |
Shared content sanitization |
courseconfig/ |
Course configuration contracts and revisions |
http/ |
Root routing, frontend authentication, errors, and request IDs |
courses/ |
Course settings, discovery, structure, progress, and caches |
feedback/ |
Feedback documents, visibility, and email delivery |
grading/ |
Rubrics, queues, assessments, assignments, and publication |
submissions/ |
Submission authorization, previews, archives, and caches |
templates/ |
Template catalogs, archives, matching, and enrichment |
manualgrading/ |
Deferred capabilities, callbacks, and delivery recovery |
aplus/ |
A+ transport, wire mapping, and port implementation |
outbound/ |
Shared bounded outbound resources and response handling |
persistence/ |
Database lifecycle and schema initialization |
preferences/ |
User-scoped preferences |
validation/ |
Shared input validation |
Backend ArchitectureTest checks package ownership and A+ internal isolation. Frontend checks cover server-only generated types, remote entry ownership, and client bundle boundaries.
Kotlin API reference
Section titled “Kotlin API reference”KDoc describes the contracts between backend features. It covers the four A+ ports, feature services, typed command outcomes, lifecycle rules, and the concurrency or security guarantees that callers must preserve. Private helpers, database tables, wire DTOs, and straightforward data containers rely on names and types instead of explanatory comments.
Generate the reference locally with:
cd backend./gradlew dokkaGeneratePublicationHtmlDokka writes the site to backend/build/documentation/kdoc. CI publishes the same directory as the backend-kdoc artifact, and the documentation image serves it under /reference/kotlin/. Dokka rejects malformed or unresolved documentation. Detekt checks sentence endings, stale parameter documentation, invalid property references, and missing documentation on supported service and port boundaries.
The generated KDoc does not duplicate HTTP reference material. Use the generated OpenAPI documents and the backend API reference for routes, authentication, headers, payloads, and status codes. Architecture and security explanations stay in this documentation site.
Writing KDoc
Section titled “Writing KDoc”Document behavior that a caller cannot learn from the signature. This includes authorization, ownership, lifecycle, cancellation, concurrency, ordering, limits, and failures that can escape the declaration. Use links to related declarations and add @throws only for exceptions that callers may receive.
Prefer a clearer name, type, or visibility when it can remove the need for a comment. KDoc must describe the current contract, not the reason a declaration changed. Private helpers, data containers, route installers, wire DTOs, and persistence tables usually do not need comments.
The KDoc guide defines syntax and links. Kotlin’s API documentation guidelines cover summaries and contract details. Dokka’s Gradle configuration reference and Detekt’s comment rules describe the checks used by this repository.