-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
A 10,000-foot tour of how skulid is built and how data flows through it.
┌─────────────────────────────────────────────────────────────────┐
│ skulid (one Go binary) │
│ │
│ HTTP layer (chi) Engines Workers │
│ ┌──────────────┐ ┌────────────┐ ┌───────────────┐ │
│ │ Web UI │ │ Sync rule │◄────┤ Per-account │ │
│ │ (htmx/ │────────►│ engine │ │ goroutine │ │
│ │ Alpine.js) │ │ │ │ pool │ │
│ └──────────────┘ └────────────┘ │ │ │
│ ┌──────────────┐ ┌────────────┐ │ Polling │ │
│ │ Webhook │────────►│ Smart │◄────┤ fallback │ │
│ │ /api/... │ │ block │ │ (5 min) │ │
│ └──────────────┘ │ engine │ │ │ │
│ ┌──────────────┐ └────────────┘ │ Watch │ │
│ │ AI assistant │ ▲ │ renewal │ │
│ │ /assistant │──────┐ │ │ (24h before │ │
│ └──────────────┘ │ │ │ expiry) │ │
│ ▼ ▼ └───────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Repositories (pgx-backed) │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ Postgres │ │ Google │
│ │ │ Calendar │
│ Token │ │ API │
│ ciphertexts│ │ │
│ stored │ │ │
│ here. │ │ │
└────────────┘ └────────────┘
| Layer | Choice |
|---|---|
| Language | Go 1.26+ |
| HTTP |
chi router + stdlib html/template
|
| Frontend | Server-rendered HTML, sprinkles of HTMX and Alpine.js |
| Database | Postgres 16 via pgx/v5 (no ORM) |
| Migrations |
goose, embedded with embed.FS
|
| Calendar API | Google Calendar API v3 (official Go client) |
| OAuth | golang.org/x/oauth2 |
| Token sealing | AES-256-GCM with per-row nonces |
| Sessions | HMAC-SHA256-signed cookies |
| Container | Multi-stage build → distroless static-debian12
|
| Table | Purpose |
|---|---|
setting |
Owner identity (TOFU), external URL, global buffers, schema version |
account |
One Google account; sealed tokens; per-account Working/Personal/Meeting hours |
calendar |
Each visible calendar; per-cal hours overrides + per-cal buffers + optional default category |
sync_token |
Per-calendar Google sync token + push channel state |
sync_rule |
A rule mirroring source → target. Visibility preset + all-day mode + working-hours-only + optional category pin |
event_link |
Links a source event to its mirror; loop-guard primary key |
smart_block |
A focus-block recipe (target, sources, working hours, horizon) |
managed_block |
Each focus block we've actually written to Google |
category |
Eight built-in categories (slug, name, color); user-editable name+color |
task |
One-shot scheduled work; priority + duration + due + scheduled placement |
habit |
Recurring soft block (e.g. Lunch); ideal_time + flex + days_of_week |
habit_occurrence |
Per-day instance of a habit (event id + window) |
task_chunk |
One calendar block of a task; a long task has several |
buffer_event |
Tracks visible Decompress and Travel buffers around non-managed meetings |
audit_log |
What skulid did and why |
ai_conversation |
One AI assistant chat (30-day TTL) |
ai_message |
One turn within a conversation |
ai_pending_action |
Tool call awaiting human confirmation |
- Google → POST
/api/webhooks/googlewithX-Goog-Channel-Id. - Webhook handler verifies the per-channel HMAC token in
X-Goog-Channel-Token, looks up the matchingsync_tokenrow, enqueues a job onto the per-account worker. - Per-account worker picks up the job, calls Calendar API
Events.listwith the stored sync token (incremental sync). - For each changed event, the rule engine processes it: looks up
matching rules, applies filter+transform, inserts/updates/deletes the
mirror via
Events.insert/update/delete, records anevent_link. - Smart-block engine recompute is debounced (15s) per affected block,
then runs: pulls busy windows via
Freebusy.query, subtracts from working hours, diffs against existing managed blocks.
If the webhook is dropped (network glitch, channel expired), the
polling fallback picks up the slack: every 5 minutes the scheduler
walks sync_token rows whose last_polled_at is stale and triggers an
incremental sync regardless.
Every write to Google sets extendedProperties.private:
{
"skulidManaged": "1",
"skulidRuleId": "42",
"skulidSourceEventId": "abc123"
}Smart-block writes use skulidSmartBlockId instead of the rule
fields. When the corresponding webhook bounces back, the rule engine
sees IsManaged() == true and refuses to forward it — that's the
primary loop guard. The event_link table is the secondary guard for
bidirectional rules: if the source etag hasn't changed since the last
sync, the engine skips the update.
- One goroutine per Google account. Jobs queue per-account so a slow account never blocks the others.
- One global scheduler runs:
- Polling fallback (5 min)
- Watch-channel renewal (every hour, gated by 24h-before-expiry)
- AI conversation cleanup (every 6h, drops chats idle >30d)
- Daily maintenance tick (every 6h) — re-runs
PlaceHabitfor every enabled habit andPlaceTaskfor any pending or expired-scheduled task, so rolling horizons stay current.
- Smart-block recompute is debounced 15s per block.
- Buffer recompute (decompress + travel) is debounced 15s per calendar; fires after every successful incremental sync of that calendar.
- All Google API calls are wrapped in
context.Contextwith sensible per-request deadlines.
cmd/skulid/main.go # entrypoint, wires every dependency
internal/
config/ # env-var loading
crypto/ # AES-256-GCM token sealing
db/ # pgx repos + scanned struct models
dbtest/ # throwaway Postgres harness for tests
auth/ # OAuth, sessions, TOFU, middleware
calendar/ # Google Calendar client (calendar.API) + extendedProperties helpers
calfake/ # in-memory calendar.API for tests
category/ # pure event-classification heuristics
hours/ # pure WorkingHours + window arithmetic + slot finders
sync/ # rule engine + smart-block engine + scheduler (tasks/habits)
worker/ # per-account workers + scheduler tick + AI cleanup
webhook/ # Google push notification handler
httpx/ # chi router, html/template, handlers
ai/ # Anthropic-powered assistant (optional)
migrations/ # *.sql, embedded into the binary
wiki/ # this documentation, synced to GitHub Wiki
See Development for hacking on the codebase.