# Implementation guide ยท Appwrite 1.9.6 and Hermes Status: implementation scaffold, verified against source contracts on 2026-09-06. No homelab connection or real GPU run has been tested. This guide supersedes the generic stack recommendations in original-design-brief.md. ## 1. Minimal deployment Reuse the existing Appwrite instance and reverse proxy. Add one application API/worker service, with a verified Hermes HTTP integration and per-host GPU workers as needed. Do not require a new PostgreSQL, NATS, MinIO or reverse proxy merely because they appeared in the earlier generic brief. Appwrite already supplies accounts, databases, storage and optional realtime notifications. Do not reuse its internal Redis as an application broker. ```mermaid flowchart TD UI["Private Assistant"] --> AUTH["Appwrite Account"] UI --> API["Authenticated application API"] API --> AW["Appwrite records + private files"] API --> WORK["Durable job worker"] WORK --> H["Hermes adapter / replaceable harness"] H --> G["Inference gateway + GPU leases"] G --> A["Arc chat route"] G --> X["Exclusive XTX work route"] WORK --> I["Immich scheduling adapter"] ``` Hermes runs tools on its own host/workspace, even when inference runs remotely on a GPU machine. Run coding tools in a properly scoped workspace. An API model route does not relocate shell commands or deploy permissions. The first implementation may have one active scheduler process. Appwrite 1.9.6 transactions exist, but GET-then-PATCH by itself is not an atomic work claim. Multiple workers require a tested transactional claim/conflict protocol and fenced physical resource leases. Never keep a database transaction open while waiting for inference, tool execution or model loading. ## 2. Appwrite project and schema Use a dedicated project, or an explicitly assigned project/database, on the existing **1.9.6** installation. Register the real web hostname as an Appwrite Web platform, including localhost for local development. Configure a same-site custom Appwrite API domain where feasible to avoid third-party-cookie failures. Test authentication on the user's iPhone/Safari as well as desktop. Keep the prototype private when connecting real data. Copy .env.example to .env and fill values locally. No server credentials belong in apps/web. The public project ID and API endpoint are not secrets. Provisioning key scopes (no data seeds): - databases.read, databases.write - collections.read, collections.write - buckets.read, buckets.write The legacy attribute and index endpoints use collections scopes. Do not substitute newer TablesDB scope assumptions. ```sh npm run appwrite:plan # Review the target project and plan; this next command changes that project. npm run appwrite:apply ``` The apply script checks /health/version equals 1.9.6, adds missing resources, waits for attributes/indexes to become available, and rejects schema/security drift. Re-running against a matching schema is safe. It does not create accounts, projects, user documents or data seeds; it never deletes or edits existing definitions. Creation is not one atomic migration: a failure can leave a partially created schema that the next run completes. Back up existing project data before any later migration. The runtime key needs documents.read/write and files.read/write for the implemented features. Remove unused scopes. Revoke the bootstrap key after provisioning. Never use either key as proof of user identity. | Collection | Purpose / key constraints | | --- | --- | | conversations | Owner, title and history ordering | | messages | Owner and conversation; unique conversation + sequence | | runs | Accepted jobs; unique owner + requestId for admission idempotency | | events | Durable normalized events; unique run + sequence for replay | | artifacts | File metadata and provenance; unique bucket + file | | context_snapshots | Immutable portable task context; no credentials | | harness_profiles | Immutable profile revisions and verified connection state | | model_routes | Independent versioned model routes and context budgets | | settings | One owner settings document, revision guarded | | gpu_status | Last observed state/heartbeat, distinct loaded and active models | Opaque JSON fields carry versioned extension data, not unbounded prompts. Enforce the declared field sizes; store larger content as private files and keep a reference. Index owner/foreign-key/state/order fields, not payload JSON. Every user-scoped server query must filter and verify ownership. Paginate: Appwrite defaults do not return every record. collections and bucket have no broad permissions; documentSecurity/fileSecurity are enabled. Server-created user-visible messages/runs/files may receive only read("user:VERIFIED_USER_ID"). Internal context, credentials references and scheduler metadata stay server-only. Never grant client write access to job state, approvals, model loading, or progress merely to simplify the frontend. ## 3. Authentication and API boundary The integration-client.mjs browser helper is supplied but deliberately not wired into the simulation. Implement sign-in using Appwrite Account sessions, then obtain a short-lived JWT with POST /account/jwts. In 1.9.6 the default lifetime is 900 seconds; keep it in memory and refresh through the active session. Do not store it in runs or URLs. An expired user token must not terminate an already authorized background job; the worker uses its own narrowly scoped server credential. On every application API call: 1. Validate the token by calling GET /account with X-Appwrite-Project and X-Appwrite-JWT, **without X-Appwrite-Key**. 2. Use the returned $id as ownerId; ignore any client-supplied identity. 3. Load/check ownership and the requested operation's policy before a privileged server-key call. 4. Validate body sizes, enums and resource IDs. Never allow a browser to choose arbitrary server fetch destinations. For a same-site BFF cookie design, implement HttpOnly/Secure cookies, CSRF/origin checks and logout propagation instead. Do not combine two incomplete auth models. A JWT cannot be read as an unverified payload and trusted. Proposed application endpoints (to implement; these are not native Appwrite or Hermes paths): | Endpoint | Meaning | | --- | --- | | GET/POST /api/conversations | Owned history / create conversation | | GET /api/conversations/:id/messages?cursor=... | Ordered persisted messages | | POST /api/conversations/:id/messages | Admit a message with stable requestId; return persisted message and run IDs | | GET /api/runs?cursor=... | Owned jobs and current statuses | | GET /api/runs/:id/events?after=... | Replay app events, then live stream; owner checked | | POST /api/runs/:id/cancel | Request cancellation, not immediate success | | POST /api/runs/:id/approval | One pending action's explicit user decision | | GET /api/artifacts/:id/download | Owner-checked byte stream, never a filesystem path | | GET /api/system | Authorized, observed GPU/model/queue status | | GET/POST /api/harnesses | Admin profile listing/registration | | POST /api/harnesses/:id/validate | Backend capability/route/permission checks | | PUT /api/harness-selection | Expected settings revision + default/role overrides | | GET/POST /api/model-routes | Admin dynamic model registry | Use authenticated fetch streaming for SSE with a bearer header, or same-site BFF cookies. Native EventSource does not accept arbitrary authorization headers. Appwrite Realtime may notify that state changed, but is not a replay log or job queue. Re-query records after reconnect and recreate subscriptions when the user session changes. ## 4. Hermes setup and contract Check the installed Hermes Git SHA/version before changing anything. The supplied client targets the official native HTTP API described by the pinned sources in SOURCES.md. Capabilities have no stable release-version field; record both capability output and installed commit. An older installation may require an explicitly reviewed adapter change or upgrade; do not silently upgrade it. Merge config/hermes.env.example into the appropriate protected Hermes profile environment. Generate its API_SERVER_KEY locally using openssl rand -hex 32. Startup is **hermes gateway**. There is no assumed hermes serve or hermes api-server command. The .env HERMES_API_KEY value used by the application equals this inbound Hermes API key. Upstream inference credentials are a separate configuration. By default the native API binds 127.0.0.1:8642. If the application and Hermes are separate containers/hosts, use a deliberate private bind/network and firewall; localhost in one container is not another container. Do not expose Hermes directly to the browser or internet. Preserve existing agent profiles, memory and workspaces. Run npm run probe after filling .env. It only reads Appwrite version and Hermes capabilities. It does not send prompts, create sessions or touch GPUs. A successful probe is not an end-to-end integration test. The supplied HermesClient implements: - GET /v1/capabilities, feature checks before execution. - POST /api/sessions with {title}; the returned ID is **response.session.id**. - POST /v1/runs with {input, session_id, model: VERIFIED_ROUTE_ALIAS} and a persisted Idempotency-Key. - GET /v1/runs/:id; GET /v1/runs/:id/events; POST /v1/runs/:id/stop. - GET /api/sessions/:id/messages with explicit pagination, for reconciliation. - POST /v1/runs/:id/approval with the exact pending request_id and once/deny choice, after application authorization. Use one native session for serialized turns under a compatible original harness/profile/route. Reuse session_id and omit conversation_history on normal continuation. Switching harness or incompatible role context creates a fresh native session from a deliberate portable handoff; it must not overwrite an existing session with a partial transcript. Persist the map from app conversation/run to native session/run separately. Runs SSE frames contain data: JSON with a **payload.event** field such as message.delta, tool.started, tool.completed, approval.request, run.completed or run.failed. They are not the named assistant.delta events of the separate session-chat streaming endpoint. The provided parser handles the runs format. Do not execute a tool again because its completed event appears. Keep one Hermes event consumer per run in the worker and fan out **persisted application events** to browsers. Native run SSE has no verified Last-Event-ID replay guarantee and may lose deltas on disconnect. Reconcile through getRun and native session messages. EOF is not completion. Terminal native states include completed, failed, cancelled and interrupted; interrupted can indicate a gateway restart. A stop response saying stopping is not proof that the tool/workspace can be handed over. Hermes run idempotency must be feature-detected. The supplied client and probe require supported=true, durable=true and a positive retention_seconds before admitting runs. Persist the same key and exact input for retries; the inspected implementation advertises 86400 seconds retention. Appwrite admission deduplication remains the long-term authority. Outside retention or with uncertain acceptance, reconcile rather than submit a duplicate. Do not assume session creation is idempotent; persist/reconcile its result too. ## 5. Delegation, model swapping and context Native Hermes delegate_task has no per-child model argument; children use its configured delegation model/provider/base_url. Route aliases select incoming runs, not an arbitrary model for each delegated subtask. Therefore implement an authorized application enqueue tool/MCP tool that submits a job with a work role, or dispatch separate verified role profiles/runs. Restrict native delegation if it would bypass this policy. Do not claim that config/hermes-routes.example.yaml alone solves multi-model scheduling. Map work role to harness revision and model route independently. A coordinator on Arc can enqueue research or coding jobs and continue chat while the XTX queue advances. Two heavyweight models that do not fit together cannot run simultaneously; show waiting_for_resource or an explicitly selected alternate route. Do not promise zero model-loading latency. All inference calls, including subagents, retries and summarization, must pass through the enforced route/lease policy. Verify the effective provider/model because Hermes session overrides or route aliases can outrank request fields. Keep local-only as default; no silent cloud fallback. The example route gateway is an implementation requirement, not an existing service implied by the YAML. Pin each accepted request to context snapshot, harness profile revision, model-route revision, tool policy and originating message. Existing queued/running/paused/retrying jobs retain these bindings after defaults change. Keep old profile revisions/services available until drained. The app transcript and artifacts are portable; hidden native memory and in-flight inference state are not. The physical GPU daemon must own load/unload/inference leases with fencing generations and heartbeat state. A stale heartbeat means unknown/unavailable; it never means safe to unload. Confirm drain/cancellation, save a valid task/workspace checkpoint, unload, verify freed memory, load the next model, validate readiness/context, then resume that run. Do not let tool workers make unfenced direct GPU calls. The first production target should be stable chat + one XTX specialist. Introduce preemption and Immich sharing only after restart and overload tests pass. Record measured VRAM/KV/context limits; do not infer 65k availability from marketing context windows. Existing XTX stability concerns make real stress/thermal verification a concrete deployment gate. ## 6. Artifacts, progress and safe rendering Upload worker outputs to the private Appwrite bucket, then store metadata: owner, conversation, run, filename, MIME, size, checksum, bucket and file IDs. The supplied uploader is explicitly limited to 5 MiB; larger uploads require a verified chunked implementation/SDK. The example bucket allows 20 MiB subject to the server's storage limit. No universal large-file uploader is claimed here. After owner authorization, proxy /storage/buckets/B/files/F/download to the user. The file metadata endpoint does not return bytes. Use Content-Disposition with a sanitized filename, nosniff and private/no-store caching. Open safe content in a separate preview/origin as appropriate; treat HTML/SVG/script-capable artifacts as downloads or sandbox them. For small files, the supplied browser blob helper works; for large files, use a streaming same-site authenticated download without exposing tokens in the URL. Keep failures visible and preserve partial-job results. Progress has known completed/total units or a qualified estimate; unknown percent is null. Show phase, queue reason, last heartbeat and elapsed time. Do not turn token count into a fake percentage. Batch text deltas before Appwrite writes; persist critical lifecycle/tool/approval events and final output. Use monotonically increasing per-run sequence numbers for application replay; never reuse a native stream offset as proof of durable storage. Do not send model output to createAssistantElement's raw HTML string interface. Replace that boundary with safe text/structured rendering or sanitized Markdown. The existing demo uses trusted scripted HTML, which is not a safe production renderer. The export service worker only caches an explicit public shell list and bypasses authenticated/API/file requests. ## 7. Replace the prototype deliberately | Current code in apps/web/app.js | Production replacement | | --- | --- | | loadModels/loadHarnesses + localStorage | Admin-owned Appwrite-backed registry/settings | | state.jobs and initialConversation seeds | Empty/auth-loading state, then owned persisted records | | chooseReply and the 850ms reply timer | POST accepted message; subscribe to app run events | | createJob | API admission with atomic deduplication and immutable bindings | | advanceRunningJobs and setInterval | Worker progress/heartbeat events; no fabricated fallback | | toggleJob / activateNextJob | Authorized job commands with asynchronous acknowledgments | | syncActiveModels / renderSystemStatus | Observed GPU heartbeat snapshots, loaded vs active vs unknown | | jobResultContent / deliverJobResult | Persisted artifact metadata and authenticated byte delivery | | applyHarnessSettings | Revision-checked server preview/commit; pin existing runs | | appendAssistantMessage | Safe streaming renderer plus durable ordered message persistence | | Share / Copy Codex handoff | Local help/download links; never publish private transcripts | Preserve the DOM structure and styling where practical. A frontend framework rewrite is optional, not a prerequisite. Separate demo and production entrypoints so production cannot accidentally seed fake runs or register a simulation response handler. ## 8. Production deployment and rollback The included compose file runs the prototype only. For production, Codex must add the actual API and singleton worker entrypoints, pin tested dependency/image versions, route /api through the existing proxy, disable response buffering for SSE, and set suitable idle timeouts. Keep long-running work independent of request/Function/browser lifetimes. Use health/readiness endpoints that distinguish Appwrite, adapter, runtime and worker failures. Use private service networking and least-privilege secret injection. Back up Appwrite's supported database/storage state and Hermes's supported profile/session data before configuration changes; protect any backup that includes credentials. Keep prior service configuration and image versions for rollback. A rollback must not replay completed side effects or downgrade the schema blindly. Preserve a tested restoration path before enabling unattended deployment tools.