Getting started
TinyCld is a pnpm workspace that ties together a set of independent repositories: an Expo/PocketBase app shell (the tinycld repo) with @tinycld/core nested inside it, and a set of feature packages (@tinycld/mail, @tinycld/contacts, @tinycld/calendar, @tinycld/drive, and more) that the app opts into at build time. A checkout with just the tinycld shell (no feature packages) runs as a lean shell with zero features — you clone exactly the feature packages you want to work on, and the generator wires in whichever are present.
Fresh-machine setup
Use @tinycld/bootstrap’s assemble-only mode to lay down the workspace — it writes the workspace coordination files (package.json, pnpm-workspace.yaml, tinycld.packages.ts, vitest.config.ts, shared test stubs, version pins) from embedded templates and clones the tinycld repo (which carries @tinycld/core nested inside it) plus any features you name with --with — then run a single pnpm install at the root:
mkdir ~/code/tinycld && cd ~/code/tinycld
# Assemble the workspace root + clone the tinycld shell + the package(s) you want.
# A subset is fine; you can always add more later.
npx @tinycld/bootstrap@latest --assemble-only --with mail --with contacts
pnpm install # links members + runs the generator (postinstall)
cd tinycld && pnpm run dev
The root pnpm install creates the node_modules/@tinycld/* symlinks for every present member and runs the generator via the postinstall hook. You do not have to clone every feature — the generator scans whichever member directories are present, and the app boots as a lean shell when none are. To add another feature later, re-run npx @tinycld/bootstrap@latest --assemble-only --with <pkg> — it skips members that already exist and clones the new one alongside.
The dev loop
cd ~/code/tinycld/tinycld
pnpm run dev # runs the generator, then starts Expo + PocketBase
pnpm run checks # biome lint across every member + tsc on the app
pnpm exec tinycld-pkg test --all # vitest across every present member
pnpm exec tinycld-pkg test:e2e --all # playwright across every present member
cd tinycld && pnpm run dev runs the package generator first, then starts the normal Expo dev server, a local PocketBase instance, and a single-port HTTP proxy that fronts both — open http://localhost:7100 (or https:// if a localhost cert is present at tinycld/assets/localhost.pem) and the app talks to PB same-origin via /api. The generator produces re-exports under tinycld/app/a/[orgSlug]/<slug>/..., writes tinycld/tinycld.config.ts (the typed installed-package source of truth), and symlinks package migrations into tinycld/server/pb_migrations/. You don’t run it manually — the dev launcher and pnpm install (via postinstall) both invoke it automatically.
Per-member checks
The tinycld-pkg CLI runs checks scoped to a single member, or across every present member with --all. From any member directory:
cd ~/code/tinycld/contacts
pnpm exec tinycld-pkg check # typecheck + unit tests for this member
pnpm exec tinycld-pkg test # unit tests only
pnpm exec tinycld-pkg test:e2e # playwright specs for this member
tinycld-pkg check runs tsc --noEmit followed by vitest run — it does not invoke Biome (Biome runs only ecosystem-wide via pnpm run lint from tinycld/). From anywhere, tinycld-pkg <verb> --all runs the verb against only the members that are present. Feature unit/e2e tests are discovered automatically from each member’s tests/ directory; cloning or removing a feature changes the run with no config edit.
Local database
The app shell ships three scripts for managing the local PocketBase database. Run them from tinycld/; each loads tinycld/.env for superuser credentials and defaults to the dev PocketBase at http://127.0.0.1:7100.
cd ~/code/tinycld/tinycld
pnpm run db:reset # wipe server/pb_data, re-run migrations, seed a test user + org
pnpm run db:seed # seed into the current database without wiping it
pnpm run db:reset:demo # reset only the singleton demo org (preserves the demo user)
db:reset is the one you reach for most. It deletes server/pb_data, boots PocketBase to apply every present package’s migrations against a fresh store, then seeds a test user (user@tinycld.org) and org. Use it after editing a migration — PocketBase doesn’t hot-reload collection-rule changes from a previously-applied migration, so a reset is what makes the new schema and auth rules take effect.
db:seed populates an existing database without dropping it: it creates the seed user and org, then calls each present package’s seed function in turn (sample contacts, a starter mailbox, demo calendar entries, …). It takes a --mode flag — --mode test (the default) creates user@tinycld.org with a primary org plus a second acme org for cross-org testing, while --mode demo creates the demo@tinycld.org singleton used by the hosted demo. See Seed data for how a package contributes its own seed function.
db:reset:demo wipes only the data scoped to the singleton demo org and re-seeds it, leaving the demo user in place. It’s built to run nightly so the hosted demo workspace is pristine for the next unauthenticated visitor; you rarely need it during local development.
Logging in
db:reset and db:seed print a boxed summary of how to log in at the end of the run, so you never have to dig through the seed script. It looks like this:
┌─────────────────────────────────────────────────────────────────────────┐
│ Seed complete — log in with: │
│ │
│ App (sign in to TinyCld) │
│ http://localhost:7100 │
│ user: user@tinycld.org │
│ password: Dev7f3a…! │
│ │
│ Superuser (PocketBase /_/ dashboard, /setup to manage orgs & packages) │
│ http://localhost:7100/setup │
│ user: admin@tinycld.org │
│ password: Tc!Xa1b… │
│ │
│ Org: test-org │
└─────────────────────────────────────────────────────────────────────────┘
There are two accounts:
- App user (
user@tinycld.org) — the account you sign in to TinyCld with. Whendb:resetcreates this user fresh and you haven’t set a password, it generates a random one and prints it in the box above. SetTEST_USER_PW(and optionallyTEST_USER_LOGIN) intinycld/.env, or pass--user-pw, to pin a known password instead — that’s what CI does. - PocketBase superuser (
admin@tinycld.org) — the admin account for the PocketBase dashboard at/_/and the TinyCld superuser dashboard at/setup(where you manage organizations and install packages). Like the app user, its password is generated and printed whendb:resetcreates it and you haven’t set one. Pin it (so it’s stable across resets) by settingADMIN_USER_PW(and optionallyADMIN_USER_LOGIN) intinycld/.env— that’s what CI does.
Ecosystem layout
The workspace root holds the coordination files: package.json (member devDeps + coordination scripts), pnpm-workspace.yaml (the authoritative member list, including the nested tinycld/package-scripts entry), tinycld.packages.ts (the present-member enumerator), vitest.config.ts, shared unit-test stubs under tests/, and pinned .node-version / .go-version. All of these come from @tinycld/bootstrap’s embedded templates — there is no separate workspace meta-repo to clone. Every other member is its own git repo, cloned alongside as a sibling directory:
~/code/tinycld/ # workspace root (bootstrap-assembled; commit it to your own repo)
package.json # member devDeps + coordination scripts
pnpm-workspace.yaml # the authoritative member list (all possible members)
tinycld.packages.ts # member enumeration for the generator
vitest.config.ts # workspace-wide vitest entry
tests/ # shared unit-test stubs (expo-router, lucide, …)
.node-version # pinned Node version
.go-version # pinned Go version
tinycld/ # the Expo/PocketBase app shell (own repo)
core/ # @tinycld/core - shared TS + Go library (nested)
package-scripts/ # @tinycld/package-scripts - the tinycld-pkg CLI (nested)
contacts/ # @tinycld/contacts ─┐
mail/ # @tinycld/mail │
calendar/ # @tinycld/calendar │ feature packages, each its own repo
drive/ # @tinycld/drive │
calc/ # @tinycld/calc │
text/ # @tinycld/text │
google-takeout-import/ # ─┘
The tinycld repo is always cloned; it carries @tinycld/core nested inside it. Each feature is its own git repo with its own history, issues, and CI, and you only clone the ones you intend to work on. pnpm-workspace.yaml lists every possible member, but pnpm tolerates members whose directories are absent — so a partial checkout installs and runs cleanly. @tinycld/package-scripts lives inside the app shell (tinycld/package-scripts/) and rides along with the tinycld clone; it’s registered as the nested member tinycld/package-scripts in pnpm-workspace.yaml.
How the workspace, core, the app shell, and features relate
@tinycld/core is a nested member at ~/code/tinycld/tinycld/core/, inside the tinycld repo. It holds the runtime: React Native, Expo Router, the PocketBase client, pbtsdb, shared UI components, theming, auth. Every feature package peer-depends on those; none of them ship their own copies. Features import from @tinycld/core/lib/* and @tinycld/core/ui/*, never the other way around. The app shell (the tinycld repo root) owns the bundler config, the generator, and the Expo Router app/ tree, and consumes @tinycld/core like any other member.
Features do not depend on each other. If the takeout importer wants to know whether mail is installed, it reads the runtime package registry (usePackages()) rather than taking a compile-time import on @tinycld/mail. This keeps each feature independently releasable and keeps the lean-shell guarantee intact — a checkout with no feature packages still typechecks and runs.
Where to go next
- Adding a package if you want to bring an existing feature package into this checkout.
- Creating a package if you want to scaffold a new one.
- Anatomy of a package if you want to understand what a package actually contains.