Repository navigation
Project Structure
ContribKit is a pnpm-workspace monorepo with three components that share design tokens.
ContribKit/
├── web/ Astro + TypeScript on Cloudflare Workers (contribkit.app + API)
├── app/ Flutter iOS & Android app, Android home-screen widgets
├── shared/ Single source of truth: palettes, shapes, usernames (JSON)
├── scripts/ Repo tooling (e.g. sync-shared-assets.mjs)
└── .github/ CI/CD workflows
| Directory | Component | Stack |
|---|---|---|
web/ |
contribkit.app + SVG/JSON API | Astro · TypeScript · Cloudflare Workers |
app/ |
iOS & Android app, Android widgets | Flutter · Riverpod · RevenueCat |
shared/ |
Palettes, shapes, usernames | JSON consumed by both apps |
domain/
value-objects/ Username, Year, ContributionLevel, Palette, CellShape, Color, IsoDate, Embed, ContactMessage, calendar-labels
entities/ ContributionDay, ContributionCalendar (types.ts)
repositories/ ContributionRepository, ContactMessageRepository (interfaces only)
services/ calendar-grid, svg-geometry, cell-shapes, dates, contribution-stats, SvgRenderer type
failures/ Failure union + constructors + isFailure, isFailureKind
application/
use-cases/ loadInitialContributions, sendContactMessage; resolve-initial-view.ts, which is request policy rather than a use case
http/ failure-http (statusFor, messageFor, fieldFor, errorBodyFor, reasonFor, retryAfterHeader); failure-log (logContributionsFailure, logContactFailure, logServerError, SERVER_ERROR_STATUS); cache-control
infrastructure/
github/ githubHtmlContributionRepository (HTML scraping)
email/ the send_email Contact Message repository, its React Email template and MIME
rendering/ svgStringRenderer
errors/ errorMessageOf, the one reading of a thrown value's message
logging/ the log contract and the console writer Cloudflare exports
ui/
components/ Astro components (core/, grid/, error/, icons/, features…)
utils/ page-init, render/state, roving, url/cookie, mulberry, …
styles/ @layer-based global CSS
pages/ every non-underscore file here is a public URL, .md included
index.astro landing page (SSR + client interactivity)
api/ contributions.ts, contact.ts, health.ts
user/ [username].svg.ts
_contributions.ts, _contact.ts shared compositions, not routes
_tests/ the route tests, kept out of the namespace
404.astro, 500.astro, contact.astro, legal-notice/privacy/terms
middleware.ts rate limiting + security headers
Unit tests sit next to what they cover. The one exception is docs/docs-consistency.test.ts: its subject is the documentation, not a module, so it lives beside the documents at the repo root. It still runs from the web package: web/vitest.config.ts adds ../docs/**/*.test.ts, web/tsconfig.json includes it, and the biome scripts pass ../docs.
Every layer carries a colocated AGENTS.md with what an implementer there needs, and the docs-consistency test fails if one is missing; the rules are in CODING_STANDARDS.md. See Architecture and Web Application.
domain/ Entities, value objects, repository interfaces, services, failures
application/ Use cases: fetch and invalidate contributions, export, tips, send a Contact Message
infrastructure/ GitHub repo, asset repos, export (png/svg/markdown), persistence, tip, contact, telemetry, http
ui/ Features (viewer, customizer, export, tip, contact, privacy, home_screen_widget), widgets, theme, DI
See Mobile App.
The single source of truth for data used by both apps:
| File | Contents |
|---|---|
palettes.json |
Every color palette, one color per Contribution Level plus the light-theme none
|
shapes.json |
Every cell shape |
usernames.json |
Suggested usernames |
Edit the JSON here, never the copies under
app/assets/.
-
web imports these directly via the
@sharedalias at build time. -
app (Flutter) can only bundle assets inside its own package, so it uses generated copies in
app/assets/*.json. They are regenerated:- automatically on commit (lefthook
pre-commitrunsscripts/sync-shared-assets.mjs --stagewhen ashared/*.jsonis staged), - manually with
pnpm sync:assets.
CI does not regenerate them, so the only thing standing between a stale mirror and a green CI run is that pre-commit hook (and the docs-consistency test, which compares the two directories and fails when they drift). The release workflow is the exception:
release-app.ymlcopiesshared/*.jsonintoassets/in itsSync shared assetsstep, immediately before building the AAB, so a shipped build is never stale even when the commit is. - automatically on commit (lefthook
Repo-wide Node scripts, invoked by Git hooks and CI:
| Script | Purpose |
|---|---|
sync-shared-assets.mjs |
Copies shared/*.json → app/assets/*.json (--stage re-stages them). Exposed as pnpm sync:assets. |
See Git Hooks for how these run.
-
Package manager: pnpm workspaces (
pnpm-workspace.yaml) - Commits: Conventional Commits, enforced by commitlint
-
Releases: semantic-release per component (
web-vX.Y.Z/app-vX.Y.Ztags) -
CI: one
ci.ymlwith no path filter. Achangesjob decides which client was touched and gates every other job on it, so nothing depends on a filter being kept in step. -
Git hooks: lefthook (installed by the root
preparescript onpnpm install) runs formatting, linting, and commit-message checks locally (see Git Hooks)