Architecture
Read-only inventory snapshot
This document is a read-only inventory snapshot of the inklet stack, dated 2026-07-13. It captures the state of the codebase at that time for team reference and new-hire onboarding, and should not be treated as live truth — re-run the inventory after version bumps or component changes.
inklet is a cloud-connected e-ink information panel platform: the backend renders structured data into 800×480 black-and-white / 4-level-gray bitmaps and pushes them to e-ink devices via AWS IoT Core. The repo is a multi-component monorepo: a Go backend + a Python render/AI worker + a React frontend + a device agent (Python) / simulator (Node) + AWS IoT infrastructure scripts.
Core path in one line: structured data → render_task/SQS → Jinja2/WeasyPrint render → Floyd–Steinberg dithered packing → S3 → MQTT notifies the device to pull and display.
System Overview
┌──────────────┐ HTTPS ┌──────────────┐ PostgreSQL ┌─────────┐
│ portal-web │ ──────────────▶│ backend │ ──────────────────▶│ RDS │
│ (React SPA) │ │ (Go / Chi) │ └─────────┘
└──────────────┘ └──┬────────┬──┘
│ SQS │ MQTT (X.509 mTLS)
┌────────┴───┐ ┌───┴────────┐
│ worker │ │ AWS IoT │
│ (Python) │ │ Core │
│ render/AI │ └───┬────────┘
└────┬───────┘ │ MQTT (X.509 mTLS)
│ S3 ┌───┴────────┐
┌────┴───┐ │ e-ink │
│ S3 │◀────│ devices │
└────────┘pull └────────────┘
Top-level Repository Layout
| Directory / Repo |
Role |
Language / Stack |
Purpose |
backend/ |
Server API |
Go 1.24 / chi / GORM |
Device access, auth, push, render_task lifecycle, billing |
worker/ |
Background worker fleet |
Python 3.12 |
Render pipeline + AI summary/vision + analyze Agent + scraping |
html_template/ |
Template library |
HTML + Jinja2 + CSS |
templates/<slug>/{config.json, template.html, schema.json} |
portal-web/ |
Web frontend + deploy orchestration |
React 19 / Vite / TS |
User console + docker-compose/nginx/caddy/certbot |
eink-hw/ (a.k.a. sim-hw) |
Device firmware (simulated) |
Python 3.11 |
One process = one device, AWS IoT provisioning + heartbeat + display |
eink-sim/ (a.k.a. sim-dashboard) |
E-ink simulator |
Node/TS Fastify + React |
Renders the 1bpp framebuffer in a browser as an e-ink display |
awsiot/ |
AWS IoT infra scripts |
Bash + JSON |
Fleet Provisioning by Claim, policies, claim cert |
fake-backend/ |
Local cloud mock |
Python 3.11 |
Subscribes to heartbeats, replies with downlink commands for end-to-end tests |
InkletDemo/ |
3D spatial demo |
React / Three.js |
Visualizes device placement in a 3D room model (not on the core path) |
docs/ |
Docs site |
MkDocs (Material) |
Architecture / API / ops docs (this document lives here) |
Backend (Go) — backend/
- Language / framework: Go 1.24, HTTP router chi/v5 (+ go-chi/cors), standard
net/http, port 4000. Entry cmd/server/main.go.
- Database: PostgreSQL 16, ORM via GORM (
gorm.io/driver/postgres, pgx v5 underneath), migrations via GORM AutoMigrate + a little raw SQL for indexes. Key tables: users / oauth_accounts / sessions / subscriptions / devices / device_commands / files / raw_upload_items / html_templates / pushes / render_tasks / content_blocks.
- Queue: AWS SQS (self-wrapped
queue.SQSQueue), four queues: SQS_PREPARE_URL (item prep) / SQS_RENDER_URL (render) / SQS_ANALYZER_URL (analyze) / SQS_SUMMARY_URL. Render flow: backend creates render_task(READY) → enqueues to render queue → worker renders and calls back POST /internal/pushes/{id}/rendered → backend promotes the push to the device's current and notifies it over MQTT.
- Object storage: AWS S3 (aws-sdk-go-v2), presigned upload/download URLs, optional CloudFront signing (RSA). Bucket via
S3_BUCKET / S3_REGION / S3_ENDPOINT (can point to minio).
- IoT / devices: MQTT 3.1.1 over TLS (mTLS X.509),
eclipse/paho.mqtt.golang, against AWS IoT Core. Offline check every 30s (2min threshold). Three binding flows: NFC signature / 6-char claim code (10min) / JWT on the NFC tag.
- Public API: REST/JSON, JWT (HS256, golang-jwt) Bearer auth + OAuth (Google/Apple). Main route groups:
/auth, /auth/subscription (Stripe subscription/checkout/portal/invoices + webhook), /api/devices (device/health/state/nickname/bind-unbind/command/push queue/custom-image upload), /api/raw-items, /api/files/{id}/download, /api/analyze/trigger, /internal/* (worker callback, X-Internal-Token).
- internal/ packages:
auth / billing / config / database / file / iot / item / middleware / model / pkg / queue / storage.
MQTT Topic Structure
| Topic |
Direction |
Purpose |
inklet/dev/{thing}/up/heartbeat |
Device → Backend |
Periodic heartbeat |
inklet/dev/{thing}/up/state |
Device → Backend |
Device state report |
inklet/dev/{thing}/up/request_claim |
Device → Backend |
Request a claim code |
inklet/dev/{thing}/down/cmd |
Backend → Device |
Commands (text, claim_code, bound, unbound) |
Render Worker (Python) — worker/
- Packaging: Python 3.12 (pyproject/setuptools), entry
inklet-worker (workers/cli.py). Multi-mode single process: render / summary / download / analyze, SQS polling + ThreadPool concurrency. Docker: python:3.12-slim + Pango/Cairo/gdk-pixbuf + fonts-dejavu-core (no CJK fonts).
- Render pipeline (
workers/render_worker.py): fetch render_task (JOIN html_templates + pushes) → if CUSTOM_IMAGE download from S3, else locate the template dir by config.title string match → Jinja2 (autoescape=False) render → WeasyPrint HTML→PDF → PyMuPDF (fitz) rasterize (96dpi) PNG → Pillow resize 800×480 (LANCZOS) → grayscale → Floyd–Steinberg dithering → pack 1bpp (image2.raw) + 2bpp (4-level gray image4.raw) + png → S3 render/{user}/{device}/{push}/ → write files + update pushes/render_tasks.
- AI side:
summary_worker (text summary + gpt-4o vision description, JSON output) / download_worker (firecrawl link scraping) / analyze_worker (OpenAI Agents SDK agent loop, tools read notes/templates/devices and create_push). LLM calls go through litellm (OpenAI by default, switchable to Anthropic), models via LLM_MODEL / LLM_VISION_MODEL / AGENT_MODEL; prompts in worker/prompts/.
- Shares the same PostgreSQL (psycopg2,
DATABASE_URL) and SQS queues with the backend.
Web Frontend — portal-web/portal-web/
- Framework: React 19 + TypeScript + Vite 6, styling with Tailwind CSS 4, routing react-router 7, toasts via sonner, pnpm package manager.
- Features: device/display management (list/detail/nickname), push content queue, Knowledge (AI-summarized knowledge items), subscription billing, account/sessions/OAuth callbacks, light/dark theme.
- Build/deploy: multi-stage Dockerfile (Node 22 pnpm build → Nginx Alpine, port 3000). Domains
portal.iminklet.com→3000, auth.iminklet.com→4000 (Caddy/Nginx reverse proxy + Let's Encrypt certbot).
eink-sim (simulator frontend) and InkletDemo (Three.js 3D demo) are two separate Vite/React apps — tools/demos, not the user console.
Device / Firmware Side — eink-hw/ (simulated)
- Python 3.11, deps
awsiotsdk/awscrt (MQTT) + Pillow. One process simulates one 800×480 1bpp e-ink device; first boot runs AWS IoT Fleet Provisioning by Claim to obtain a permanent cert, then a steady-state loop sends heartbeats (30s), receives downlink commands, renders text, and HTTP POSTs the framebuffer (48000B) to eink-sim.
- Protocol: MQTT (AWS IoT Core) toward the cloud; HTTP/WebSocket toward the simulator. Real hardware panel target is 800×480 monochrome.
awsiot/ is Bash scripts that one-shot set up the IoT role/policy/provisioning template/claim cert.
Device Lifecycle
Factory → NFC tag with signed hw_id
↓
Power on → Fleet Provisioning (claim cert → device cert)
↓
Heartbeat loop → Backend registers device in DB
↓
User binds device (NFC or claim code)
↓
Backend sends commands → Device renders to display
Deployment & Runtime
- docker-compose (
portal-web/docker-compose*.yml): postgres:16 + backend (ghcr.io/inklet-2026/backend) + portal-web; the prod variant adds an internal network, mounted secrets, 127.0.0.1-only bindings, restart policies.
- External services: PostgreSQL (dev container / prod RDS), AWS S3 (+CloudFront), AWS SQS, AWS IoT Core (MQTT), OAuth (Google/Apple), Stripe (billing), OpenAI/Anthropic (LLM), Firecrawl (scraping).
- CI/CD: GitHub Actions
release.yml (one each for backend/worker/portal-web), triggered on v* tags → build/test → push ghcr.io/inklet-2026 images (version + latest). No enforced pre-merge checks; releases go through manual tags.
- See
portal-web/DEPLOY.md for deployment and RUNBOOK.md for local integration.
Known Constraints & Risks
- WeasyPrint engine: CSS 2.1+, no JS, incomplete flex/grid — templates must be written within these limits.
- No CJK fonts in the container: the worker image ships only DejaVu, so Chinese renders as tofu boxes (affects Chinese content rendering; to be handled separately).
- Fixed 800×480 canvas: overflow is clipped, templates must be designed to this size.
- No enforced pre-merge checks: releases go through manual
v* tags with no gate — merge quality relies on manual review.
Key Dependency Version Table
Current locked versions read directly from go.mod / pyproject.toml / package.json / Dockerfile at inventory time (2026-07-13), for reviewer alignment — transitive deps not exhausted.
Backend backend/ (Go)
| Component / Library |
Version |
| Go |
1.24.0 |
| go-chi/chi |
v5.2.1 |
| gorm.io/gorm |
v1.25.12 |
| gorm.io/driver/postgres |
v1.5.11 |
| jackc/pgx |
v5.5.5 (indirect) |
| aws-sdk-go-v2 (core) |
v1.41.7 |
| aws-sdk-go-v2/service/s3 |
v1.101.0 |
| aws-sdk-go-v2/service/sqs |
v1.42.27 |
| eclipse/paho.mqtt.golang |
v1.5.1 |
| golang-jwt/jwt |
v5.2.2 |
| stripe/stripe-go |
v82.1.0 |
| golang.org/x/oauth2 |
v0.29.0 |
| PostgreSQL (runtime) |
16 |
Render Worker worker/ (Python)
| Component / Library |
Version |
| Python (runtime image) |
3.12-slim (requires-python >=3.11) |
| Jinja2 |
>=3.1 |
| Pillow |
>=10.0 |
| WeasyPrint |
unpinned (Dockerfile pip install weasyprint, latest) |
| PyMuPDF |
unpinned (Dockerfile pip install pymupdf, latest) |
| boto3 |
>=1.35 |
| psycopg2-binary |
>=2.9 |
| litellm |
>=1.40 |
| openai-agents |
>=0.17.3 |
| firecrawl-py |
>=1.0 |
| cryptography |
>=43 |
Web Frontend portal-web/portal-web/ (React)
| Component / Library |
Version |
| react / react-dom |
^19.2.4 |
| vite |
^6.3.5 |
| tailwindcss / @tailwindcss/vite |
^4 |
| react-router-dom |
^7.6.1 |
| typescript |
^5 |
| sonner |
^2.0.7 |
Device eink-hw/ (Python) & Simulator eink-sim/ (Node)
| Component / Library |
Version |
| eink-hw · Python |
>=3.11 |
| eink-hw · awsiotsdk |
>=1.21.0 |
| eink-hw · awscrt |
>=0.20.0 |
| eink-hw · Pillow |
>=10.0 |
| eink-sim · fastify |
^5.1.0 |
| eink-sim · @fastify/websocket |
^11.0.1 |
| eink-sim · pngjs |
^7.0.0 |
| eink-sim · react / react-dom |
^19.0.0 |
| eink-sim · vite |
^7.0.0 |
| eink-sim · typescript |
^5.7.2 |
Stack by Role
- Backend engineer: Go 1.24 / chi / GORM+Postgres / aws-sdk-go-v2 (S3·SQS) / paho-MQTT (AWS IoT) / JWT+OAuth / Stripe. Core in
backend/internal/{iot,queue,storage,billing,auth}.
- Render worker engineer: Python 3.12 / Jinja2 / WeasyPrint / PyMuPDF / Pillow (dithering · 1bpp/2bpp) / SQS (boto3) / psycopg2; AI part litellm / OpenAI Agents / firecrawl. Remember WeasyPrint limits + no CJK fonts + fixed 800×480 canvas.
- Frontend engineer: React 19 / TS / Vite / Tailwind4 / react-router; talks to the backend REST + JWT.
- Device / firmware engineer: Python (awsiotsdk) + AWS IoT Fleet Provisioning + MQTT topic conventions; simulator side Node/Fastify + React + WebSocket (1bpp frame protocol).
- Ops: docker-compose + Nginx/Caddy + certbot; AWS (IoT/S3/SQS/CloudFront), RDS Postgres; GHCR + tag-triggered GitHub Actions;
awsiot/ infra scripts.
- Reviewer / new hire: start with this document +
RUNBOOK.md + portal-web/DEPLOY.md; the core path is the one-liner at the top.