This page describes source behavior and configured boundaries, not a successful runtime exercise or human acceptance. Use Status for deployment evidence, Quickstart for visitor setup, and Capabilities for the feature map.
Request path and edge boundary
The configured path is browser → nginx TLS and HTTP Basic → loopback 127.0.0.1:52300 → container maic port 3000 → Next standalone node server.js.
The edge, port mapping, and entrypoint are defined in deploy/nginx/maic.loca.zone.conf:20-29,40-73, deploy/compose.yml:10-22, and Dockerfile:32-51.
- HTTP Basic is set at server scope for the application vhost, covering pages, assets, and APIs; its credential file is
/etc/nginx/maic.htpasswd. (deploy/nginx/maic.loca.zone.conf:20-29,40-73) - This overlay deliberately uses no Authelia gate: its recorded rationale is the shared bypass default and the decision not to mutate shared authentication configuration. (
deploy/nginx/maic.loca.zone.conf:13-19) - Per-IP limits are
20r/sfor UI traffic and2r/sfor/api/; their respective bursts are 60 and 20, with excess requests returning 429. (deploy/nginx/maic-ratelimit.conf:3-4;deploy/nginx/maic.loca.zone.conf:40-42,60-62) - The edge request-body ceiling is
32m; it is narrower than the200mbNext proxy setting. (deploy/nginx/maic.loca.zone.conf:31-33;next.config.ts:7-9) - Both proxy locations strip
Authorization, so the Basic gate credential is not passed to the application. (deploy/nginx/maic.loca.zone.conf:44-51,64-71) - The API proxy disables buffering and caching and configures 30-minute read/send timeouts for long responses. These settings are not proof that a stream completed. (
deploy/nginx/maic.loca.zone.conf:52-57)
The diagram combines that configured request path with the two source-level generation protocols.
Browser orchestration uses outline SSE followed by content/actions JSON; headless submission schedules after, while the credential-free configuration blocks its model construction.
The dotted success path is the implemented contract, not an outcome claimed for this deployment. (app/generation-preview/page.tsx:472-514,595-647; app/api/generate-classroom/route.ts:34-50; lib/server/classroom-generation.ts:178-189,410-417; deploy/maic.env.example:3-16)
flowchart LR B[Browser] --> E[TLS Basic rate limits] E --> L[127.0.0.1:52300] L --> N[maic node server.js] N --> O[Outline SSE] O --> C[Content and actions JSON] C --> D[Browser IndexedDB] N --> H[Headless POST] H --> A[202 plus jobId] A --> R[next after runner] R --> X[Blocked missing key] A --> P[Poll status] X --> P P -. success only .-> J[Classroom JSON]
The browser-save destination in the diagram is IndexedDB, not the host data bind; successful headless output instead uses server classroom storage.
The failed-job branch records an error for polling. (lib/store/stage.ts:249-264; lib/utils/stage-storage.ts:36-71; lib/server/classroom-generation.ts:410-417; lib/server/classroom-job-runner.ts:34-41)
See Security for the boundary’s limitations and Operations for service administration.
Generation protocols
Browser-driven generation
- Home assembles a generation session and puts it in
sessionStorage; the preview page drives the subsequent requests. (app/page.tsx:233-297;app/generation-preview/page.tsx:472-484) - Preview sends the selected model and provider configuration using
x-model,x-api-key,x-base-url, and related headers. Browser-supplied keys therefore pass through the server. (app/generation-preview/page.tsx:91-116;lib/server/resolve-model.ts:64-71) POST /api/generate/scene-outlines-streamreturnstext/event-stream; preview reads the response body and parsesdata:events into outlines. This is SSE over a POST response, not a JSON response or anEventSourceGET. (app/api/generate/scene-outlines-stream/route.ts:350-355;app/generation-preview/page.tsx:472-514)- For the first outline,
POST /api/generate/scene-contentreturns JSON containingcontentandeffectiveOutline. (app/generation-preview/page.tsx:593-619;app/api/generate/scene-content/route.ts:139-162) POST /api/generate/scene-actionsconsumes that content, creates actions, assembles the scene, and returns JSON containingsceneandpreviousSpeeches. (app/generation-preview/page.tsx:625-647;app/api/generate/scene-actions/route.ts:128-153)- Preview saves the first scene locally, retains remaining outlines, transfers continuation parameters, and navigates to
/classroom/{id}. (app/generation-preview/page.tsx:708-726) - The classroom page can resume unfinished scene or media work; remaining scenes call the content/actions APIs through the scene-generation hook. Opening a classroom is not necessarily passive playback. (
app/classroom/[id]/page.tsx:103-149;lib/hooks/use-scene-generator.ts:33-118)
Those generation routes resolve a model and invoke provider-backed generation; browser orchestration does not make the LLM computation local.
The route implementations keep outline streaming distinct from the two JSON-returning scene stages. (app/api/generate/scene-outlines-stream/route.ts:103-104,248-255; app/api/generate/scene-content/route.ts:74,139-162; app/api/generate/scene-actions/route.ts:78,128-153)
Headless generation: implemented, blocked here
The headless entrypoint is POST /api/generate-classroom, not the browser’s /api/generate/* sequence.
It accepts a required requirement plus selected PDF, language, enrichment, and agent-mode fields. (app/api/generate-classroom/route.ts:11-31)
- Submission creates a persisted queued job and returns HTTP 202 with top-level
jobId,status,step,message,pollUrl, andpollIntervalMs: 5000. (app/api/generate-classroom/route.ts:34-50;lib/server/api-response.ts:43-44) - The route registers
after(() => runClassroomGenerationJob(...))fromnext/server; the runner marks the job running and invokes generation in the Node process. (app/api/generate-classroom/route.ts:1,39;lib/server/classroom-job-runner.ts:23-34) - Generation calls
resolveModel({}), so this path uses server configuration rather than the visitor’s browser BYOK settings. (lib/server/classroom-generation.ts:178;lib/server/resolve-model.ts:23-44) - On a configured successful path, it generates outlines, builds scenes sequentially, optionally generates media/speech, and persists classroom JSON before reporting completion. (
lib/server/classroom-generation.ts:237-306,311-435) GET /api/generate-classroom/{jobId}returns progress, result/error, anddone; onlysucceededandfailedare terminal. (app/api/generate-classroom/[jobId]/route.ts:26-38)- A successful result identifies the classroom and view URL; JSON can then be retrieved with
GET /api/classroom?id={classroomId}. (lib/server/classroom-job-store.ts:196-212;lib/server/classroom-storage.ts:80-83;app/api/classroom/route.ts:40-61)
D-088 intentionally leaves server provider credentials and DEFAULT_MODEL empty.
With that configuration, the resolver chooses implicit OpenAI gpt-4o-mini, then throws because the provider key is missing, before the classroom pipeline can run. (deploy/maic.env.example:3-16,22-30; lib/server/resolve-model.ts:30-31; lib/ai/providers.ts:944-947)
A 202 can therefore acknowledge a queued job without enabling generation: the runner catches the missing-key exception and records a failed job.
This is a configuration-derived expectation, not a recorded request result. (app/api/generate-classroom/route.ts:34-50; lib/server/classroom-job-runner.ts:23-41)
The runner’s active-job map and job-store locks are process-local; persisted status is not a durable work queue or restart replay mechanism.
Polling computes failure for a running job whose timestamp is over 30 minutes old, without writing that computed stale result back. (lib/server/classroom-job-runner.ts:11-49; lib/server/classroom-job-store.ts:59-96,124-130)
Two persistence domains
Visitor browser
Ordinary browser-generated classroom work lands in browser IndexedDB, while settings and drafts use browser storage; it is not automatically written to the container’s data bind.
Homepage recents enumerate that browser’s stages rather than a server classroom catalogue. (lib/store/stage.ts:249-264; lib/utils/stage-storage.ts:36-71,137-159; lib/store/settings.ts:944-946)
The Dexie database is MAIC-Database, and schema version 8 declares all ten tables below.
Table presence describes the data model, not a guarantee that every feature actively restores every record. (lib/utils/database.ts:176-193,298-310)
| Table | Stored responsibility | Source |
|---|---|---|
stages | Classroom metadata and current scene identifier | lib/utils/database.ts:41-50,300 |
scenes | Scene content, actions, whiteboards, ordering, and timestamps | lib/utils/database.ts:55-66,301 |
audioFiles | TTS audio blobs with format and optional speech metadata | lib/utils/database.ts:71-80,302 |
imageFiles | Image blobs with filename, MIME type, size, and creation time | lib/utils/database.ts:85-92,303 |
snapshots | Legacy undo/redo snapshots | lib/utils/database.ts:188,304 |
chatSessions | Messages, session configuration, tool calls, and pending calls | lib/utils/database.ts:97-111,305 |
playbackState | Per-stage playback snapshot record | lib/utils/database.ts:114-122,306 |
stageOutlines | Per-stage generation outlines | lib/utils/database.ts:125-132,307 |
mediaFiles | Generated image/video blobs, posters, prompts, parameters, and errors | lib/utils/database.ts:137-152,308 |
generatedAgents | Per-stage generated agent profiles | lib/utils/database.ts:157-167,309 |
The playbackState schema does not justify promising playback-position resume: the current Stage component explicitly removed that persistence and leaves the restoration branch empty. (components/stage.tsx:18,408-421)
The current localStorage keys have separate responsibilities:
| Key | Stored responsibility | Source |
|---|---|---|
settings-storage | Provider configurations including BYOK keys, model and audio/media choices, and preferences | lib/store/settings.ts:28-149,421-477,944-946 |
user-profile-storage | Local avatar, nickname, and biography | lib/store/user-profile.ts:20-42 |
agent-registry-storage | Agent registry; generated agents instead reload per stage from IndexedDB | lib/orchestration/registry/store.ts:203-257 |
theme | Theme preference | lib/hooks/use-theme.tsx:24-26,53-55 |
locale | Interface language | lib/hooks/use-i18n.tsx:12-40 |
webSearchEnabled | Homepage web-search preference | app/page.tsx:52,99-105 |
generationLanguage | Generation-language preference | app/page.tsx:53,99-105 |
recentClassroomsOpen | Recent-classroom section state | app/page.tsx:54,94-97 |
requirementDraft | Requirement draft | app/page.tsx:80-82 |
quizDraft:${sceneId} | Scene-specific quiz draft | components/scene-renderers/quiz-view.tsx:694-701 |
pblChatDraft | PBL chat draft | components/scene-renderers/pbl/chat-panel.tsx:33-37 |
Legacy reads also exist for llmModel, providersConfig, ttsModel, selectedAgentIds, and maxTurns; they are not the current settings-store name. (lib/store/settings.ts:353-368; lib/ai/providers.ts:853-857)
The separate sessionStorage keys are generationSession for the preview workflow and generationParams for classroom continuation. (app/page.tsx:254-295; app/generation-preview/page.tsx:714-724; app/classroom/[id]/page.tsx:117-138)
Browser persistence is not a claim that data never leaves the browser: generation sends requirements, context, and locally supplied provider credentials to API handlers. (app/generation-preview/page.tsx:91-116,472-482)
Server filesystem
Server paths are derived from process.cwd()/data; the runner’s working directory is /app.
D-088 binds /home/loca/dev/maic/data to /app/data, separately from all visitor browser stores. (lib/server/classroom-storage.ts:6-7; Dockerfile:32; deploy/compose.yml:21-22)
| Container path | Server-side records | Source |
|---|---|---|
/app/data/classrooms/{id}.json | Persisted {id, stage, scenes, createdAt} classroom | lib/server/classroom-storage.ts:6,37-42,61-78; Dockerfile:32 |
/app/data/classrooms/{id}/media/ and audio/ | Headless-generated media and narration files referenced by scenes | lib/server/classroom-media-generation.ts:61-75,109-125,155-158,209-210,241-253; Dockerfile:32 |
/app/data/classroom-jobs/{jobId}.json | Job status, progress, input summary, timestamps, and optional result/error | lib/server/classroom-storage.ts:7; lib/server/classroom-job-store.ts:17-57,105-120,196-225; Dockerfile:32 |
Headless submission can write job metadata even when later generation is blocked.
There is also a manual server-write path: POST /api/classroom persists supplied stage/scenes without using the headless generator. (app/api/generate-classroom/route.ts:34-39; app/api/classroom/route.ts:11-29)
Classroom loading checks local IndexedDB first and falls back to GET /api/classroom; this does not turn the recent-classroom list into shared server discovery. (app/classroom/[id]/page.tsx:35-59; lib/utils/stage-storage.ts:137-159)
Classroom/job JSON writes use a sibling temporary file followed by rename.
Job records retain an input summary, not the full generation request, so the data bind alone cannot replay interrupted work. (lib/server/classroom-storage.ts:21-28; lib/server/classroom-job-store.ts:17-57,105-120; lib/server/classroom-job-runner.ts:11-49)
Provider and model resolution
For routes using resolveModel, model selection is explicit request model → DEFAULT_MODEL → gpt-4o-mini.
The parser interprets providerId:modelId by splitting only the first colon; an unqualified model is assigned to openai, and later colons remain in the model ID. (lib/server/resolve-model.ts:23-31; lib/ai/providers.ts:1041-1059)
- Without a client base URL, a nonempty client API key wins over the matching server key. (
lib/server/resolve-model.ts:41-43;lib/server/provider-config.ts:235-239) - Supplying a client base URL disables server-key fallback: only the client key is passed onward, preventing implicit use of a server key at that endpoint. (
lib/server/resolve-model.ts:33-44) - Base URL precedence is client → server → provider registry default → SDK default. (
lib/server/resolve-model.ts:44;lib/server/provider-config.ts:241-245;lib/ai/providers.ts:952-954) - Header-based callers read
x-model,x-api-key,x-base-url,x-provider-type, andx-requires-api-key; the last field only forwards an explicit true override. (lib/server/resolve-model.ts:64-71) - The implicit OpenAI default still requires a key; it is not a free fallback or automatic selection of whichever provider is configured. (
lib/server/resolve-model.ts:30-31;lib/ai/providers.ts:944-947,1055-1059)
This is not one global default for every endpoint: /api/chat separately uses body.model || 'gpt-4o-mini' and constructs its model directly, without consulting DEFAULT_MODEL. (app/api/chat/route.ts:65-100)
Browser BYOK persists in settings-storage, and preview obtains its request headers from that settings-backed model configuration. (lib/store/settings.ts:944-946; lib/utils/model-config.ts:6-21; app/generation-preview/page.tsx:91-103)
Server provider configuration combines optional YAML with environment fields; nonempty environment values override matching YAML fields, and configuration is cached in-process. (lib/server/provider-config.ts:139-177,208-216)
The credential-free D-088 template deliberately supplies no server provider key; visitor BYOK and server headless configuration are distinct paths. (deploy/maic.env.example:3-19,22-30; lib/server/classroom-generation.ts:178)
Runtime file dependencies
Prompt loading reads Markdown synchronously from process.cwd()/lib/generation/prompts/ and caches loaded templates/snippets.
The loader expects templates/{promptId}/system.md, optionally user.md, and snippets/{snippetId}.md; failure to load a system template returns null. (lib/generation/prompts/loader.ts:17-44,61-94)
The outline generator treats a missing prompt as an error, so template availability is a runtime dependency, not merely authoring material. (lib/generation/outline-generator.ts:116-117)
Optional provider YAML is read at process.cwd()/server-providers.yml.
A missing, unreadable, or invalid file yields an empty YAML configuration, after which environment configuration remains available. (lib/server/provider-config.ts:101-113,139-165,174,208-216)
The D-088 Compose overlay mounts only the data directory, not a provider YAML file. (deploy/compose.yml:13-22)
The Docker runner explicitly copies public, .next/standalone, and .next/static; it does not explicitly copy the prompt tree or YAML.
Next configuration has no explicit prompt outputFileTracingIncludes entry. Automatic tracing of the dynamic prompt paths is not established by this source-only account. (Dockerfile:43-45; next.config.ts:3-10; lib/generation/prompts/loader.ts:65-77)
Consult Status for any separately recorded runtime-file evidence; no container inspection or generation success is asserted here.
Container and service shape
| Boundary | Configured implementation | Source |
|---|---|---|
| Image and container | D-088 references maic-showcase:d088 as container maic | deploy/compose.yml:10-12 |
| Base and package manager | node:22-alpine; build stages activate pnpm 10.28.0 | Dockerfile:2-5,30 |
| Build output | Next selects standalone output when VERCEL is unset; Docker copies that standalone tree | next.config.ts:3-4; Dockerfile:43-45 |
| Runtime identity | nextjs has UID 1001; the image creates the nodejs group with GID 1001 | Dockerfile:40-47 |
| Process | Working directory /app, production Node environment, port 3000, node server.js | Dockerfile:32-36,49-51 |
| Host exposure | Only loopback 127.0.0.1:52300 is published to container port 3000 | deploy/compose.yml:19-20 |
| Persistence | Host data directory binds to /app/data; it does not include browser IndexedDB or localStorage | deploy/compose.yml:21-22; lib/utils/database.ts:176-193; lib/store/settings.ts:944-946 |
The dependency stage installs native build tools and Cairo/Pango/image development libraries; the runner installs corresponding runtime libraries and libc6-compat.
These support native dependencies including sharp; PDF parsing imports sharp and uses it to encode extracted image data as PNG. (Dockerfile:12-13,38; lib/pdf/pdf-providers.ts:140-141,221-233)
maic.service is a oneshot unit with RemainAfterExit=yes: start runs Compose up -d, stop runs down, and reload runs restart.
Compose, not a foreground systemd Node process, owns the configured container lifecycle and unless-stopped restart policy. (deploy/maic.service:7-16; deploy/compose.yml:23)
Normal server persistence requires the bound data directory to be writable by the runtime process; this page does not claim runtime verification of those permissions. (Dockerfile:40-47; lib/server/classroom-storage.ts:9-28)
Related pages
- Quickstart: visitor setup and first-use guidance.
- Capabilities: feature boundaries and credential requirements.
- Operations: deployment lifecycle and storage handling.
- Security: edge protection and application risk boundaries.
- Status: orchestrator-recorded evidence, separate from this static architecture map.