Tessera¶
Tessera is a multi-pod issue tracker with a kanban board, filterable issue list, full-text search, issue pages with comments, an activity feed, and analytics.
Architecture¶
The application (examples/tessera.yaml) consists of four WebAssembly pods (each under a single-replica Deployment), three Services, and an Ingress. The Deployment replaces deleted or evicted pods, and its Recreate strategy stops the old pod before starting the new one so a node never holds two copies:
flowchart LR
Browser["Browser"] -->|"/, /api, /events"| Ing["Ingress: tessera"]
subgraph CP["Control plane (PSRAM)"]
Ing -->|"/"| SvcWeb["Service: tessera-web"] --> Web["Pod: tessera-web"]
Ing -->|"/api"| SvcApi["Service: tessera-api"] --> Api["Pod: tessera-api"]
end
subgraph Worker["Cardputer worker"]
SvcDb["Service: tessera-db"] --> Db["Pod: tessera-db"]
Db --> SD[("microSD PVC: tessera-db")]
Hud["Pod: tessera-hud"]
end
Api -->|"HTTP"| SvcDb
Hud -->|"tessera.input"| Api
Api -->|"tessera.events"| Ing
Api -->|"tessera.events"| Hud
Ing -->|"tessera.events"| Hud
| Pod | Node | Memory limit | Role |
|---|---|---|---|
tessera-web |
Control plane | 4Ki |
Serves the single-page web client from images/web/tessera-web/ via k32_resp_asset (apps/tessera/web). |
tessera-api |
Control plane | 16Ki SRAM, 1280Ki PSRAM |
Holds the in-memory store and search index. Answers /api/* requests (apps/tessera/api). Runs with kube32.io/execution-budget: "off". |
tessera-db |
Cardputer | 8Ki |
Store of record (apps/tessera/db). Appends and reads records on the microSD volume claim tessera-db (/sd/kube32/volumes/tessera-db/). |
tessera-hud |
Cardputer | 1Ki |
k32_tick/k32_event pod (apps/tessera/hud). Subscribes to tessera.events via kube32.io/event-topics, draws open issues, request rate, and viewer count on the LCD, and reads the keypad. |
Services and Ingress routing:
Service/tessera-web,Service/tessera-api, andService/tessera-dbexpose port 80 targeting container porthttp(7000).Ingress/tesseraroutes/apitotessera-apiand/totessera-web.tessera-webserves compressed bundle assets viak32_resp_assetwith gzip./and client routes such as/boardreturnindex.htmlwithCache-Control: no-cache, while versionedapp.jsandstyles.cssURLs (?v=...) useCache-Control: public, max-age=31536000, immutable.- The Ingress serves
GET /api/cluster(kube32.io/cluster-telemetry) and streamstessera.events,kube32.cluster, andkube32.presenceas Server-Sent Events on/events(kube32.io/event-stream). - Once per second, the Ingress publishes request rate and viewer counts to
tessera.events(kube32.io/stats-topic). tessera-hudpublishes keystrokes ontessera.input.tessera-apisubscribes viakube32.io/event-topics: tessera.inputand appends characters to a live note on the most recently viewed issue. Pressing Enter stores the note as a comment bykeypad.tessera-hudis silent by default (HUD_SOUND: "0"and no speaker grant inexamples/tessera.yaml). To enable event and keystroke tones, setHUD_SOUNDto"1", requesthardware.kube32.io/speaker: "1", and add theSPEAKERcapability.
Storage and write-through flow¶
tessera-db stores records in 32 KiB segment files (00000.seg, 00001.seg, up to 256 segments or 8 MiB) on the microSD card. Each record has a 12-byte header with a sequence number and CRC-32, and a payload of at most 1,280 bytes (TLOG_PAYLOAD_MAX). Each append request accepts up to 2,048 bytes (TDB_APPEND_MAX). On startup, tessera-db verifies the last segment and pads any interrupted tail in place.
tessera-api keeps the serving copy and inverted index in its 1,280 KiB PSRAM arena (default capacities MAX_ISSUES=2900, MAX_COMMENTS=5300, MAX_TERMS=1024):
- On startup,
k32_tickfetches/statsfromhttp://tessera-db, reads records in 16 KiB pages viaGET /records?after=<seq>, verifies CRCs, and applies 32 records per tick (LOAD_CHUNK). Read requests work against the loaded subset whileGET /api/statsreports"loading":{"done":N,"total":M}alongside"storage". - Write requests (
POST,PATCH,DELETE, and keypad comments) encode a log record and sendPOST /append?expect=<next_seq>&log=<id>totessera-dbviak32_http_call. tessera-apiupdates its in-memory store and emits atessera.eventsmessage only aftertessera-dbacknowledges the write. A2xxresponse means the record is on the microSD card.- While
tessera-apiis loading or whentessera-dbis unreachable, writes return503withRetry-After: 2and reads continue from memory. Iftessera-dbreturns409(sequence or log ID mismatch),tessera-apireloads the store from the log on the next tick.
Generating and loading the dataset¶
The boards do not generate seed data. An empty volume produces an empty tracker.
Generate the sample dataset on the host (2,400 issues, 4,800 comments, 9,001 records, 878,525 bytes across 28 segments):
This writes segment files to apps/tessera/build/card/kube32/volumes/tessera-db/.
Copy apps/tessera/build/card/kube32 to the root of the Cardputer microSD card, or upload the directory over the network to the running worker node (28 files, about 880 KB):
tools/kube32-cli push apps/tessera/build/card/kube32/volumes/tessera-db --node <worker-node> --path volumes/tessera-db
On the host simulator, point KUBE32_SD_ROOT at apps/tessera/build/card before starting kube32-sim. To run the worker in a separate simulator process (or attach a physical Cardputer worker), pass --workers "" on the control-plane simulator so it starts without an in-process worker. Like the boards, the simulator ticks pods that return a positive value from k32_tick every 10 ms and other pods at 1 Hz.
Deploying and signing in¶
Apply the manifest:
Open http://<control-plane-ip>/ in a browser (or http://127.0.0.1:8080/ on the simulator with --ingress-port 8080). Sign in as mara with password demo. Every user in the sample dataset shares the password demo.
To run a load test against the Ingress:
Building and testing the pods¶
Build all four .wasm modules into images/ and the compressed web assets into images/web/tessera-web/:
Run the native test suites for tessera-db, tessera-api, and tessera-web:
Inspect the memory layout and dataset sizing:
For frontend work without a cluster, run the standalone mock server:
Limitations¶
- When the search dictionary reaches
MAX_TERMS(1,024), new words in issues and comments are not indexed. - The live keypad note drops characters beyond 200 bytes until you press Backspace or Enter.