Underlayer has two deployment targets:
- Web platform — hosted service for browsing courses and managing account
- Android app — offline-first course player with download and local learning
Both are built from the same Chemical codebase. The web platform serves the API and web UI. The Android app downloads courses and provides the offline learning experience.
git push main
↓
CI: Build Chemical project
↓
CI: Run integration tests
↓
CI: Build web platform
↓
CI: Build Android APK (if app changes)
↓
CD: Deploy web to hosting
↓
CD: Upload APK to distribution
↓
CD: Update course CDN (if course content changed)
Code must be correct before merge. There is no "fix it later" — the auto-deploy pipeline means every commit goes live. This enforces:
- All tests pass before merge
- Code review before merge
- No broken builds in production
If any deployment step fails:
- Previous version stays live
- Failure is reported immediately
- Fix is committed and pipeline re-runs
- Server: Chemical
http::server::Serverwith thread pool - Database: SQLite via Turso HTTP v2 (local dev + cloud)
- Storage: Tigris S3 (course assets, APK downloads)
- Hosting: Fly.io or similar (single binary deployment)
src/main.ch (wiring only)
↓
web/ (public pages, SSR)
↓
admin/ (course management)
↓
api/ (JSON REST endpoints)
↓
content/ (course rendering, exercise engine)
↓
learning/ (FSRS, progress, review scheduling)
↓
repository/ (ALL data access)
↓
models/ (domain structs)
database/ (Turso HTTP client)
storage/ (S3 client)
↓
core/ (config, logging, utils)
| Endpoint | Method | Purpose |
|---|---|---|
/api/health |
GET | Health check |
/api/courses |
GET | List available courses |
/api/courses/:id |
GET | Course metadata |
/api/courses/:id/download |
GET | Download course zip |
/api/learner/state |
GET | Get learner state |
/api/learner/state |
POST | Update learner state |
/api/learner/review |
GET | Get due review items |
/api/learner/review |
POST | Submit review results |
/api/learner/session |
POST | Log a learning session |
- Simple token-based auth for Android app
- Web: optional account (courses can be used without account)
- Learner state is tied to device ID or account
- Language: Chemical (compiled to native via TCC or LLVM)
- UI: Native Android (JNI bridge to Chemical)
- Storage: SQLite (local database for learner state)
- Course Storage: File system (downloaded course directories)
Android UI (Kotlin/Java)
↓ JNI
Chemical Runtime
↓
Course Player (renders course content)
↓
FSRS Engine (spaced repetition scheduling)
↓
Local SQLite (learner state, review history)
↓
File System (downloaded courses)
The app works entirely offline after course download:
- First launch: Download courses from server
- Learning: All content is local, no network needed
- Review: Spaced repetition runs locally
- Sync: When online, sync learner state to server
- Updates: When online, check for course updates
User taps "Download" on a course
↓
App requests course zip from server
↓
Zip downloaded to temp directory
↓
Zip extracted to courses/<id>/
↓
Course manifest loaded
↓
Review items generated (if first download)
↓
Learner state initialized
↓
Course available offline
The course player renders course content locally:
- Read manifest.json — get concept list, metadata
- Render lesson — convert concept content to native UI
- Present exercises — interactive exercise engine
- Show visualizations — render interactive diagrams
- Track progress — record attempts, accuracy, timing
- Schedule reviews — FSRS engine computes next review times
When the app comes online:
App sends:
- Device ID
- Last sync timestamp
- Learner state (concept states, review items, session history)
Server responds:
- Updated learner state (merged from other devices if multi-device)
- Course updates (new versions available)
- New courses (if any)
Conflict resolution: server state wins. Single-device primary.
Courses are distributed as zip files:
elf-v1.zip
manifest.json
concepts/
exercises/
visualizations/
assets/
reviews/
Course zips are hosted on CDN (Tigris S3). When a course version bumps:
- New zip is uploaded to CDN
- Old zip remains available (for version pinning)
- App checks for updates on sync
When a course updates:
- App detects new version on sync
- Downloads only changed content (delta update)
- Merges with existing local copy
- Re-generates review items for changed concepts
- Notifies learner: "Course X has been updated. 3 concepts changed."
Build from the Underlayer directory itself — the project no longer needs to be linked
into the Chemical repo as lang/compiled/underlayer. database/chemical.mod imports the
sqlite3 bindings by URL (import "github.com/chemicallang/sqlite3"), which the compiler
caches under build/remote/. Only the compiler binary has to live inside the Chemical
repo (it needs import std).
# CHEMICAL=<path to the chemical repo checkout>
# Build web platform (from the Underlayer root)
$CHEMICAL/cmake-build-debug/TCCCompiler chemical.mod \
-o build/underlayer.exe -bm-modules --no-cache
# Run it (COURSES_DIR defaults to ./courses, PORT defaults to 9000)
PORT=9000 ./build/underlayer.exe
# Run the test suite (114 tests)
$CHEMICAL/cmake-build-debug/TCCCompiler chemical.mod -o build/tests.exe \
-frecompile-plugins --test --no-cache && ./build/tests.exe
# Build the HAT course's pre-rendered static pages (writes courses/hat/output/)
cd courses/hat && $CHEMICAL/cmake-build-debug/TCCCompiler chemical.mod \
-bm-modules --no-cache -o build/hat-course.exe && ./build/hat-course.exeUse --no-cache: an incremental build can miss edits to a module that already
compiled, which looks exactly like "my change did nothing". The scripts under
scripts/ still assume a lang/compiled/underlayer layout; run the commands above
directly when that symlink is not set up.
# Android (cross-compilation target, historical path)
cmake-build-debug/TCCCompiler lang/compiled/underlayer/chemical.mod \
-o lang/compiled/underlayer/build/libunderlayer.so --target android -bm-modules# .github/workflows/deploy.yml
on:
push:
branches: [main]
jobs:
build-and-deploy:
steps:
- checkout
- setup-chemical
- build-web
- test-web
- deploy-web
- build-android (if app changes)
- upload-apk
- update-cdn (if courses change)| Variable | Purpose | Default |
|---|---|---|
PORT |
Server port | 9000 |
DATABASE_URL |
Turso HTTP URL | local |
DATABASE_TOKEN |
Turso auth token | - |
STORAGE_BUCKET |
Tigris S3 bucket | - |
STORAGE_ENDPOINT |
Tigris S3 endpoint | - |
STORAGE_KEY |
Tigris access key | - |
STORAGE_SECRET |
Tigris secret key | - |
COURSE_CDN_URL |
CDN URL for course downloads | - |
- Health check endpoint:
/api/health - Deployment status: CI/CD pipeline notifications
- Course download counts: tracked in repository
- Error logging: to file, no external service dependency