Skip to content

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.