"One who knows" — in Nahuatl.
The official marketing & documentation website for Tlamatini, the local-first AI developer assistant — a React 19 single-page app with a Hono + tRPC backend, served from a Google Cloud VM at xaiht.org.
🌐 Live site · 📦 The Tlamatini app · ⚡ Quickstart · 🚀 Deploy
This is not the Tlamatini desktop application — that lives at github.com/XAIHT/Tlamatini.
This repo is the public-facing website that tells the world what Tlamatini is, shows the demos, and walks visitors through installation. It is a fully-fledged fullstack app in its own right: a React front end, a typed RPC backend, Google sign-in, a MySQL database, and a one-command Docker deploy to Google Cloud.
The header selects both the website language and the application edition:
| Selection | Application repository |
|---|---|
| English | XAIHT/Tlamatini |
| Español | XAIHT/Tlamatini-Spanish |
src/i18n/editions.ts owns each edition's version, installer, source ZIP,
repository, documentation, and issue links. Update the appropriate edition only
after checking its release assets. Never copy an English download URL into a
translated component. useEdition() selects the destinations; useT() selects
the copy. Setup instructions live in src/i18n/installation.ts.
Shareable URLs use ?lang=en or ?lang=es, retaining the route, other query
parameters, and section anchor. An explicit URL choice wins over the saved choice,
then the browser language. LocalizedLink keeps the selection on internal links.
The choice still works when local storage is blocked. Screenshots that show the
English application are labeled as such in both languages.
Run npm run check, npm run lint, npm test, and npm run build after an
edition update. Check both languages on mobile and desktop, including the actual
header control, reload, Back/Forward, downloads, and scroll-driven sections.
| 🎬 A cinematic front page | GSAP scroll-driven storytelling with three live, GPU/canvas backgrounds (moonlit ripple, silk flow-field, rain-on-glass) and a real-time moon-phase widget — all procedural, zero binary assets. |
| 🌎 Bilingual out of the box | Every UI string lives in a type-safe en / es dictionary. A header switch flips the whole site between English and Spanish; TypeScript guarantees no key is ever missing. |
| 🔐 Real auth, not a mock | Google OAuth 2.0 → signed jose JWT → httpOnly session cookie, with CSRF state protection and an owner auto-promote-to-admin rule. |
| 🧩 End-to-end type safety | tRPC 11 over Hono means the browser calls the server through fully-typed procedures — no hand-written fetch, no drift between client and API. |
| 🗄️ A managed schema | Drizzle ORM + MySQL with drizzle-kit migrations, a seed script, and per-user persistence. |
| ☁️ One-command deploy | Build a Docker image, push to Artifact Registry, restart a systemd unit on the GCP VM behind NGINX/HTTPS — automated end-to-end by a Jenkins pipeline. |
Tlamatini's pitch is that the toolbelt beats the parameter count. The website has to make that pitch felt, not just read.
So instead of a flat brochure, the front page is a scroll-driven walk through the architecture — five layers, 74 agents, 27 skills — rendered over living backgrounds. The medium is the message: a local-first tool deserves a site that does real work in your browser.
Four steps: install deps, point at a MySQL database, drop in your env vars, run the dev server.
1 · Clone & install (Node 20+):
git clone https://github.com/XAIHT/Tlamatini.git xaiht-web
cd xaiht-web
npm ci2 · Create your env file — copy the documented template and fill it in (full reference in §6):
cp .env.example .env
# then edit .env: APP_SECRET, DATABASE_URL, GOOGLE_CLIENT_ID/SECRET, PUBLIC_BASE_URL, OWNER_GOOGLE_SUB3 · Push the schema to your local MySQL (mysql://root:pass@127.0.0.1:3306/xaiht):
npm run db:push4 · Run it:
npm run dev # Vite + Hono dev server on http://localhost:3000Open http://localhost:3000 — the landing page renders without auth. Click Sign in to exercise the Google OAuth flow (you'll need an OAuth client configured, see §2.5).
Prefer the production path? Jump to §7 Build & Deploy.
📦 Recent history — click to expand
Latest —
Implementing the Spanish/English version!The whole site became bilingual: every label moved into a type-safesrc/i18n/translations.tsdictionary withenandeskeys, fronted by aLanguageProviderand a headerLanguageSwitch. TypeScript enforces parity between the two languages so a missing translation is a compile error, not a runtime blank.Before that — versioned alongside the Tlamatini app. The site tracks the product it advertises: the
/tlamatinipage mirrors the current release (v1.17.2 at time of writing — 74 agent types, 27 skills, hybrid RAG, ACPX, Unreal MCP) and its install walkthrough. Bumping the app version means refreshing the marketing copy here.
📖 The Tlamatini app & its docs · 🔧 Troubleshooting · 🤝 Contributing
- 1. Overview
- 2. Quickstart (local dev)
- 3. Project Layout
- 4. The Frontend
- 5. The Backend (Hono + tRPC)
- 6. Configuration (env)
- 7. Build & Deploy
- 8. npm Scripts Reference
- 9. Architecture at a Glance
- 10. Troubleshooting
- 11. Contributing & License
XAIHT is the website for Tlamatini (Nahuatl for "one who knows"), a local-first AI developer assistant. The site itself is a fullstack TypeScript app:
- a React 19 single-page front end built with Vite, animated with GSAP, painted over live canvas/WebGL backgrounds;
- a Hono HTTP server exposing a fully-typed tRPC 11 API plus a Google OAuth 2.0 flow;
- a MySQL database accessed through Drizzle ORM, with
drizzle-kitmigrations and a seed script; - packaged into a single Docker image and shipped to a Google Cloud Compute Engine VM behind NGINX/HTTPS by a Jenkins pipeline.
The same codebase serves both the rendered SPA (static assets) and the /api/* surface from one Node process.
License: GPL-3.0 · Live: https://xaiht.org · The product it advertises: https://github.com/XAIHT/Tlamatini.
| Layer | Choices |
|---|---|
| Front end | React 19, React Router v7, Tailwind CSS v3 + shadcn/ui (Radix primitives), GSAP (ScrollTrigger), D3 (d3-force), react-markdown + remark-gfm, TanStack Query, react-hook-form + Zod |
| API | Hono 4, tRPC 11 (@trpc/server + @trpc/react-query), superjson transformer |
| Auth | Google OAuth 2.0, jose (HS256 JWT), httpOnly session cookie, CSRF state cookie |
| Data | Drizzle ORM 0.45, mysql2, drizzle-kit migrations |
| Build/Tooling | Vite 7, esbuild (server bundle), TypeScript 5.9, ESLint 9, Prettier, Vitest |
| Deploy | Docker (node:20-alpine), GCP Compute Engine + Cloud SQL (MySQL), NGINX + HTTPS, Jenkins → Artifact Registry → systemd |
Production is a single GCP Compute Engine VM. NGINX terminates TLS and reverse-proxies to the Node container, which serves the built SPA from dist/public and the API from /api/*. The MySQL database is Cloud SQL, reached over a local cloud-sql-proxy. The systemd unit xaiht-app owns the container lifecycle. Full pipeline in §7.
| Requirement | Recommended | Notes |
|---|---|---|
| Node.js | 20 LTS+ | Matches the node:20-alpine production base image. |
| npm | 10+ | package-lock.json is committed; use npm ci for reproducible installs. |
| MySQL | 8.x | Local instance, or point DATABASE_URL at any MySQL. The schema is pushed with drizzle-kit. |
| Google OAuth client | — | A Web application OAuth client (Client ID + Secret) from the GCP credentials console. Only needed to exercise sign-in. |
You do not need the Tlamatini desktop app, Ollama, or any of its models to run this website — those belong to the product repo.
git clone https://github.com/XAIHT/Tlamatini.git xaiht-web
cd xaiht-web
npm cicp .env.example .envThen fill in the values — each one is documented there and in §6. The app will not start without DATABASE_URL; OAuth needs GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/PUBLIC_BASE_URL. .env is gitignored — never commit it.
Point DATABASE_URL at a MySQL database, then sync the schema:
npm run db:push # apply db/schema.ts directly (fast, dev-friendly)
# — or, the migration route —
npm run db:generate # emit a SQL migration from schema changes
npm run db:migrate # apply pending migrationsThe schema (db/schema.ts) defines two tables — users and notes — described in §5.4. A seed helper lives at db/seed.ts.
- Create a Web application OAuth client in the GCP credentials console.
- Add the redirect URI:
${PUBLIC_BASE_URL}/api/oauth/callback— for dev that'shttp://localhost:3000/api/oauth/callback. - Put the Client ID/Secret in
.env, and setPUBLIC_BASE_URLto exactly the origin you registered. - Sign in once, then read the
unionIdcolumn of yourusersrow (it stores the Googlesub) and setOWNER_GOOGLE_SUBto it — that account auto-promotes torole=adminon its next sign-in.
npm run devVite serves the React app and @hono/vite-dev-server mounts api/boot.ts for /api/* — both on http://localhost:3000. Hot-module reload covers the front end; the API reloads on save too.
┌──────────────────────────── http://localhost:3000 ────────────────────────────┐
│ Vite (React SPA) @hono/vite-dev-server (api/boot.ts) │
│ / → Home /api/oauth/start → Google OAuth │
│ /tlamatini → product page /api/oauth/callback │
│ /login → sign-in /api/trpc/* → tRPC router │
│ * → NotFound │
└────────────────────────────────────────────────────────────────────────────────┘
xaiht-web/
├── src/ # React front end (the SPA)
│ ├── pages/ # Home, Tlamatini, Login, NotFound
│ ├── components/ # Navigation, Footer, backgrounds, UI kit (ui/)
│ ├── i18n/ # en/es dictionary + LanguageProvider
│ ├── providers/ # TRPCProvider
│ ├── hooks/ lib/ utils/ store.ts types.ts const.ts
│ ├── config.ts # site config objects (title, header, backgrounds…)
│ └── main.tsx # entry: BrowserRouter + TRPCProvider + LanguageProvider
├── api/ # Hono + tRPC backend
│ ├── boot.ts # server entry (dev + prod)
│ ├── router.ts # tRPC root router
│ ├── auth-router.ts # notes-router.ts middleware.ts context.ts
│ ├── kimi/ # Google OAuth (auth, platform, session, types)
│ ├── queries/ # users, connection (Drizzle)
│ └── lib/ # env, cookies, http, vite helpers
├── db/ # schema.ts, migrations/, seed.ts, relations.ts
├── contracts/ # shared constants, error messages, types
├── public/images/ # hero & feature imagery
├── scripts/ # build-server.mjs + vm-*.sh deploy helpers
├── Dockerfile Jenkinsfile drizzle.config.ts vite.config.ts
└── .env.example
Path aliases (see vite.config.ts): @ → src, @contracts → contracts, @db / db → db.
main.tsx wraps the app in BrowserRouter, the tRPC/React-Query provider, and the LanguageProvider. App.tsx declares four routes. Every user-visible string flows from src/config.ts (structural config) and src/i18n/translations.ts (translated copy) — not hard-coded into components.
boot.ts builds a Hono app that mounts the OAuth handlers and the tRPC fetch adapter, then either hands the app to Vite's dev server (development) or to @hono/node-server listening on $PORT (production). The OAuth logic lives under api/kimi/ (the directory name is legacy — it is Google OAuth, not Kimi).
contracts/ holds values both client and server agree on — the session cookie name (kimi_sid), its 7-day max-age, OAuth paths, and shared error messages — so the two ends never disagree. db/ is the single source of truth for the schema.
The landing page (src/pages/Home.tsx). A GSAP ScrollTrigger storyboard: an ASCII intro, an overview band with headline stats (74 agents, 27 skills, the current version, 4096 max iterations), a vision/mission/concept card grid, a scroll-stacked walk through the five-layer architecture, the agent-family workflow, a tools grid, and the footer — all over a live procedural background.
The product page (src/pages/Tlamatini.tsx): a hero, an overview, feature cards (each with imagery from public/images/), a step-by-step installation walkthrough mirroring the app's own Quickstart (Ollama, model pulls, clone/venv/migrate/run), the agent families, and a tech-stack grid. This page is the on-site copy of the product README.
Login.tsx is a single Google Sign in card; authenticated visitors are redirected to /. Any unmatched route falls through to NotFound.tsx (404).
Three full-viewport, GPU/canvas backgrounds ship as components and need no binary assets:
| Component | Effect |
|---|---|
MoonlitRipple.tsx |
A rippling pool with a single moon reflection. |
FlowField.tsx |
A noise-driven vector flow-field ("silk"). |
RainOnGlass.tsx |
Rain on glass with droplet trails. |
MoonPhase.tsx renders the current moon phase live on a canvas. Floating panels use backdrop-filter: blur(...) over a semi-transparent base — the signature liquid-glass look. The palette is a warm gold accent (#c9a96e) over near-black, with muted green/slate-blue secondaries; type is Inter (body), IBM Plex Mono (labels), and Orbitron (display), loaded in index.html.
src/i18n/translations.ts holds one dictionary with en and es keys; LanguageProvider exposes the active language and LanguageSwitch (in the header) flips it. Because both languages are typed against the same shape, a missing or misspelled key fails the TypeScript build rather than rendering blank — see npm run check.
api/boot.ts constructs the Hono app, applies a body-size limit, and registers:
GET /api/oauth/start → begin Google OAuth (redirect to Google)
GET /api/oauth/callback → exchange code, set session, redirect to /
ALL /api/trpc/* → tRPC fetch handler (appRouter)
* /api/* → 404
In production it also serves the built SPA from dist/public and listens via @hono/node-server on $PORT (default 3000). In dev, Vite owns the listener and Hono is mounted as middleware.
The root router (api/router.ts) composes:
| Router | Procedures | Purpose |
|---|---|---|
| (root) | ping |
Health check — returns { ok, ts }. |
auth (auth-router.ts) |
me, logout |
Current session user; clears the session cookie. |
notes (notes-router.ts) |
list, create, read, update, delete, graph |
Per-user note CRUD + a wiki-link graph projection (carried from the project's note-app lineage; backed by the notes table). |
middleware.ts defines the public vs. authenticated procedures; context.ts builds the per-request context (DB handle + session user) from the cookie.
Browser ─► GET /api/oauth/start
│ set CSRF `state` cookie (httpOnly), redirect ▼
Google consent screen
│ redirect back with ?code&state ▼
GET /api/oauth/callback
│ verify state · exchange code → tokens (api/kimi/auth.ts)
│ fetch profile sub/name/email/picture (platform.ts)
│ upsert users row by unionId (queries/users.ts)
│ sign HS256 JWT (session.ts) · set `kimi_sid` cookie (7 days)
▼ redirect to /
Hardening baked in: PUBLIC_BASE_URL is required in production (blocks Host-header injection on the redirect URI), the session cookie is capped at 7 days, and the OAuth state is held in an httpOnly cookie for CSRF protection. The account whose Google sub equals OWNER_GOOGLE_SUB is auto-promoted to role=admin.
Two MySQL tables in db/schema.ts:
| Table | Columns |
|---|---|
users |
id (PK), unionId (Google sub, unique), name, email, avatar, role (user|admin), createdAt, updatedAt, lastSignInAt |
notes |
id (PK), userId, title, content, tags (JSON), source, createdAt, updatedAt |
Copy .env.example → .env. Every variable is documented there; the essentials:
| Variable | Required | Meaning |
|---|---|---|
NODE_ENV |
— | development / production. |
PORT |
— | Node listen port (default 3000). |
APP_SECRET |
✅ | HS256 signing secret for session JWTs (64-char hex recommended). Rotating it invalidates every session. |
DATABASE_URL |
✅ | MySQL DSN. Local: mysql://root:pass@127.0.0.1:3306/xaiht. Prod: via cloud-sql-proxy. |
GOOGLE_CLIENT_ID |
✅* | Google OAuth Web client ID. |
GOOGLE_CLIENT_SECRET |
✅* | Google OAuth Web client secret. |
PUBLIC_BASE_URL |
✅* | Public origin used to build the OAuth redirect_uri. Must exactly match the registered URI (http://localhost:3000 in dev, https://xaiht.org in prod). |
OWNER_GOOGLE_SUB |
— | Google sub of the account to auto-promote to admin on first sign-in. |
* Required to exercise sign-in. The landing pages render without auth.
Production values live in
/etc/xaiht/app.envon the GCP VM (seescripts/vm-update-env.sh), never in the repo.
npm run build # vite build → dist/public, then bundles api/boot.ts → dist/boot.js (esbuild)
npm start # NODE_ENV=production node dist/boot.jsscripts/build-server.mjs is the server-bundling step. The single Node process then serves the SPA and the API together.
The multi-stage Dockerfile (node:20-alpine) installs deps, runs npm run build, and ships a lean production image that runs npm start on port 3000:
docker build -t xaiht-app .
docker run --rm -p 3000:3000 --env-file .env xaiht-app Internet ──► xaiht.org (DNS)
│ HTTPS
┌───▼────────────────────────────┐
│ GCP Compute Engine VM │
│ NGINX (TLS) ─► node container │
│ │ (dist/boot.js) │
│ │ systemd unit: xaiht-app │
│ cloud-sql-proxy ──► Cloud SQL │
└─────────────────────────│───────┘
MySQL (xaiht)
The container image is pulled from Artifact Registry (us-central1-docker.pkg.dev/xaiht-492820/xaiht-images/xaiht-app). The systemd unit xaiht-app owns the container; restarting it pulls :latest and relaunches.
Jenkinsfile defines the pipeline (project xaiht-492820, region us-central1):
- Checkout — resolve the short SHA.
- Build image —
docker buildtagged:<sha>and:latest. - Push — authenticate with the
gcp-jenkins-deployer-sakey and push both tags to Artifact Registry. - Deploy — SSH to the VM as
jenkins-deployer,systemctl restart xaiht-app, and pollis-activefor up to 3 minutes. - Smoke test — curl
https://xaiht.org/until it returns200/302/304.
One-off provisioning/operations live in scripts/ (run on the VM):
| Script | Purpose |
|---|---|
vm-setup.sh |
Initial VM provisioning. |
vm-update-env.sh |
Write/refresh /etc/xaiht/app.env. |
vm-drizzle-push.sh |
Apply the Drizzle schema to Cloud SQL. |
vm-nginx-https.sh |
Configure NGINX + TLS. |
vm-rotate-secret.sh |
Rotate APP_SECRET (invalidates sessions). |
| Script | What it does |
|---|---|
npm run dev |
Vite + Hono dev server on :3000. |
npm run build |
Build the SPA and bundle the server into dist/. |
npm run images:optimize |
Regenerate full-resolution WebP images and their size/hash manifest from preserved artwork. |
npm start |
Run the production server (dist/boot.js). |
npm run preview |
Preview the built SPA. |
npm run check |
tsc -b — full type-check. |
npm run lint |
ESLint over the repo. |
npm run format |
Prettier write. |
npm test |
Vitest run. |
npm run db:push |
Push db/schema.ts straight to the database. |
npm run db:generate |
Generate a SQL migration from schema changes. |
npm run db:migrate |
Apply pending migrations. |
Website raster images are served as WebP. Original PNG/JPEG artwork remains in
images_fro-mTlamatini/, campaign/assets/gallery/, and assets/image-sources/,
outside the public output. After changing artwork, run npm run images:optimize,
then npm test and npm run build. Keep the generated images and
assets/image-manifest.json together. Tiny SVG logos and MP4 videos retain their
native formats. See Image Optimization for the
encoding policy, measured savings, and verification scope.
┌──────────────────────────────────────────────────────────────────────────────┐
│ BROWSER │
│ React 19 SPA · React Router v7 · GSAP · D3 · Tailwind/shadcn · i18n (en/es) │
│ │ typed calls │ redirect │
│ ▼ @trpc/react-query ▼ │
├──────────────────────────────────────────────────────────────────────────────┤
│ HONO SERVER (api/boot.ts) │
│ /api/trpc/* ─► appRouter { ping · auth · notes } /api/oauth/{start,cb} │
│ middleware → context (session user + DB) Google OAuth · jose JWT │
├──────────────────────────────────────────────────────────────────────────────┤
│ DATA │
│ Drizzle ORM ─► MySQL (users · notes) │
└──────────────────────────────────────────────────────────────────────────────┘
built into one Docker image ─► GCP VM (NGINX/HTTPS + systemd)
| Symptom | Fix |
|---|---|
npm run dev exits immediately |
Almost always a missing/invalid DATABASE_URL. Confirm MySQL is up and the DSN is reachable. |
OAuth redirect_uri_mismatch |
PUBLIC_BASE_URL must exactly match the redirect URI registered in the GCP OAuth client, including scheme and port. |
Signed in but auth.me returns null |
Check the kimi_sid cookie is set and APP_SECRET is identical to the one that signed it (rotating it logs everyone out). |
| Owner account isn't admin | Set OWNER_GOOGLE_SUB to the unionId value of your users row, then sign in again. |
| Type errors after editing copy | A translation key exists in en but not es (or vice versa). Run npm run check — the dictionary shape is enforced. |
| Schema changes don't take | Re-run npm run db:push (dev) or generate + migrate (prod). Confirm drizzle.config.ts points at the right DB. |
| Deploy "active" never reached | Slow Artifact Registry pull on the VM; the Jenkins poller waits 3 min. Check journalctl -u xaiht-app on the box. |
Single-branch workflow on main. Before opening a PR: npm run check && npm run lint && npm test. Keep user-visible strings in src/config.ts / src/i18n/translations.ts — never hard-code copy into components, and keep the en / es dictionaries in sync.
The marketing copy and product walkthrough describe Tlamatini — the local-first AI developer assistant this site exists to showcase.
GPL-3.0 — see LICENSE.
