Packages-first monorepo for the Greenpill Network public website, Fly-hosted agent service, shared public/private contracts, and future authenticated workspace.
packages/website: static Astro public website forgreenpill.network, with Keystatic retained for editorial/site-composition content and approved operational content consumed from public snapshots.packages/agent: Hono service scaffold foragent.greenpill.network, local Postgres readiness, and future cache/intake jobs.packages/admin: self-hosted Directus admin service fornetwork-admin/ futureadmin.greenpill.network.packages/shared: reusable payload normalization and privacy-boundary contracts.packages/workspace: placeholder for the future authenticated workspace atapp.greenpill.network.
.plans/is the durable planning, research, handoff, and design-artifact surface.- Runtime code, migrations, and shared contracts belong under
packages/. - Root-level
data/is intentionally unused; graph research data lives with.plans/backlog/knowledge-commons-graph-explorer/until that backlog work becomes product code. - Root
scripts/should stay limited to active repo validation, migration, and package tooling. One-off plan generators belong beside the plan artifacts they produce.
- Bun
1.3.10from the checked-inpackageManagerfield. - Node
>=22.12.0for Astro and local agent commands. - Docker Desktop or OrbStack for local Postgres and Directus containers.
- Fly CLI for validating and deploying the agent service.
bun install --frozen-lockfileThe root workspace owns the lockfile. Run installs from the repo root, not from an individual package.
For day-to-day local work, run the full repo environment from the repo root:
bun run devbun run dev runs the repo-native full-stack coordinator. It starts local
Postgres, runs migrations, seeds operational content, starts Directus and
bootstrap metadata, then starts the website and agent/API:
- Public Astro website:
http://localhost:3301 - Directus admin/CMS:
http://localhost:3302 - Agent/API:
http://localhost:3303 - Local Postgres:
127.0.0.1:3304
directus and agent depend on local migrations, which depend on Postgres, so
the coordinator brings the heavier infrastructure up first even though the UI
ports stay lower in the project block. The same startup path also seeds missing
published operational content into local Postgres without overwriting existing
Directus-owned records, then waits for Directus and applies the local roles,
permissions, and Data Studio metadata.
Stop the stack with Ctrl-C in the foreground terminal. The coordinator cleans up Directus and Postgres with the repo's native Docker commands.
Useful local checks:
curl http://localhost:3301/
curl http://localhost:3302/server/ping
curl http://localhost:3302/server/health
curl http://localhost:3303/health
curl http://localhost:3303/ready
curl http://localhost:3303/content/public-snapshot
curl http://localhost:3303/map/stateFor cross-repo orchestration, dev launch greenpill-network calls this same
native repo command under workbench ownership. Read workbench logs with
dev logs greenpill-network:<target> only when diagnosing a workbench-launched
target.
bun run dev:website
bun run build:website
bun run preview:websitebun run dev:website, bun run build, and bun run preview delegate to the
website package. Root bun run dev starts the full local environment: website,
Directus, agent/API, local Postgres, migrations, local content seed, and local
Directus bootstrap. The production static build emits to packages/website/dist/.
Keystatic is enabled only during local website development for editorial and site-composition authoring. It does not expose operational collections such as chapters, chapter initiatives, people, guilds, projects, or themes. There is no deployed Keystatic server, CMS API route, or public database connection in the website package.
Operational content for chapters, chapter initiatives, public steward profiles,
guilds, guild-owned projects, locations, and impact source bindings is
Directus/Postgres-owned after the one-time migration. Chapter initiatives are
local chapter-owned programs, campaigns, events, education series, cleanups,
Water Cup-style work, and impact efforts; guild projects remain
guild-owned tools, products, and workstreams. The static website consumes an
approved public snapshot at build time. By default local builds use
packages/website/src/data/operational-content-snapshot.json; production builds
can set OPERATIONAL_CONTENT_SNAPSHOT_URL to the agent route.
The seed JSON used for the one-time migration and local snapshot refresh lives in
packages/website/src/data/operational-content-seed/, outside Astro/Keystatic
editorial collections.
Public assets live in packages/website/public/. Generated public JSON routes include:
/locations.jsonfrom the approved operational content snapshot./impact-sources.jsonfrom approved snapshot chapter impact source bindings.
Website-wide design tokens and base styles live in
packages/website/src/styles/gp-tokens.css; UI changes should start from that
file, packages/website/DESIGN.md, and the existing primitives under
packages/website/src/components/ui/.
Useful operational content commands:
bun run content:snapshot
bun run content:migrate
bun run directus:content:setup
bun run directus:studio:setup
bun run directus:local:bootstrapcontent:snapshot validates current operational content and refreshes the
static fallback snapshot. content:migrate is a one-time seed into the
content schema when DATABASE_URL or DIRECT_DATABASE_URL is set; it refuses
to run once operational rows exist unless --allow-existing is passed, and even
then it only inserts missing rows without overwriting Directus-owned conflicts.
directus:content:setup runs after Directus boots and applies the steward
editor, steward moderator, trusted publisher, and operator access model through
the Directus API. directus:studio:setup applies the Data Studio labels,
field ordering, interfaces, displays, and hidden technical collections that make
the same schema usable for steward editing. directus:local:bootstrap waits for
the local Directus health endpoint, then runs both setup steps. It intentionally
ignores root .env.local so production Directus admin URLs or tokens cannot be
used by the local bootstrap path by accident; pass explicit DIRECTUS_*
environment variables from the shell only when you intentionally need an
override.
The current public site deployment can remain on GitHub Pages. Because this is a monorepo, do not use the branch/folder picker to look for packages/website/dist; GitHub Pages branch publishing only supports the repository root or /docs. In repository settings, set Pages source to GitHub Actions. The .github/workflows/github-pages.yml workflow installs from the checked-in Bun lockfile, runs bun run build:website, and publishes packages/website/dist.
If we later migrate the public site to Vercel, use the repo root as the project root, bun install --frozen-lockfile as install command, bun run build:website as build command, and packages/website/dist as output directory.
cp .env.example .env.local
bun run db:local:up
bun run db:migrate
bun run dev:agentThe local agent defaults to http://127.0.0.1:3303 with the values in
.env.example. bun run dev:agent loads root .env.local as defaults while
letting launcher-provided environment variables win, so bun run dev can keep
the agent pointed at the local Postgres target on 3304. If an older
.env.local still points DATABASE_URL at the deprecated 54329 local port,
update it to 3304.
Useful checks:
curl http://127.0.0.1:3303/health
curl http://127.0.0.1:3303/ready
curl http://127.0.0.1:3303/impact/chapters/nigeria
curl http://127.0.0.1:3303/content/public-snapshot/health is process-level health. /ready checks DATABASE_URL connectivity. /content/public-snapshot exposes only published public-safe operational content, including approved chapter initiatives for chapter detail pages. /impact/chapters/:slug, POST /map-nodes, GET /map-nodes/public, /map/state, and the map-node edit-link/update-request routes remain behind the agent privacy boundary.
Public route contract rule: every new public agent route needs an exported route
constant, a shared public payload contract in packages/shared, a public-safe
normalizer or assertion, and a focused contract test. Keep privacy filtering in
shared contracts instead of one-off route or website fetch logic.
Public map-node submissions require an owner email. The agent stores that email only in intake.map_node_private_contacts so future owner updates can use one-use email magic links. Configure email sending on the agent with RESEND_API_KEY, MAP_NODE_EMAIL_FROM, MAP_NODE_EMAIL_REPLY_TO, and MAP_NODE_EDIT_BASE_URL; do not expose those values to the static website, Keystatic, generated JSON, or browser bundles. Map magic-link replies should route to the monitored map mailbox on the verified sending subdomain, currently Greenpill Network <map@mail.greenpill.network>. Missing or failing email provider configuration still returns the same neutral public edit-link response.
For steward onboarding sessions, set MAP_NODE_STEWARD_EMAIL_ALLOWLIST on the
agent to a comma- or whitespace-separated list of email=chapter-slug entries.
Plain email entries still promote the role for compatibility, but mapped entries
also attach the trusted public chapterSlug to the steward node. Non-allowlisted
public submissions cannot self-claim steward, organizer, or coordinator roles.
Pair this with Live Onboarding Mode when steward nodes should appear publicly
during the session.
Resend delivery webhooks post to https://agent.greenpill.network/webhooks/resend and require RESEND_WEBHOOK_SECRET. Set RESEND_WEBHOOK_RECIPIENT_HASH_SECRET so recipient hashes are keyed rather than dictionary-searchable. Subscribe only to operational email events such as email.sent, email.delivered, email.delivery_delayed, email.failed, email.bounced, email.complained, email.suppressed, and email.received; do not enable open/click tracking for map magic-link emails. The webhook route verifies Svix signatures and stores provider metadata only, not message bodies, subjects, sender addresses, raw recipient addresses, or free-form provider diagnostic messages.
Run bun run db:cleanup:map-node-edit-flow from a private environment with DATABASE_URL set to delete expired edit-token rows after their grace period and scrub retained private edit-link/update-request metadata.
Directus is self-hosted as a separate admin service, not as part of the public Astro website.
bun run db:local:up
bun run db:migrate
bun run content:migrate -- --allow-existing
bun run dev:admin
bun run directus:content:setup
bun run directus:studio:setupThe local Directus service runs at http://localhost:3302 and connects to the
same local Postgres database as the agent. Use it for steward moderation,
authenticated operational content edits, owner-update review, and internal data
review only; keep public intake and public API traffic behind the agent routes.
Default local bootstrap credentials are admin@greenpill.network /
directus-local-password unless overridden before first boot.
Standard steward moderators see review-safe map-node submission/update fields.
Token rows, owner emails, IP/user-agent fields, rate-limit metadata, and raw
request metadata are reserved for trusted publisher/operator access.
When using bun run dev or dev launch greenpill-network, the local content
seed and Directus setup steps run as part of the registered stack. When running
repo scripts manually, run bun run directus:local:bootstrap after Directus is
listening to apply the same role and studio metadata.
Invite stewards without using the Directus UI by preparing a local TSV file with
email, name, location, and optional role columns:
bun run directus:users:invite -- --input /path/to/stewards.tsvThe default role is Greenpill Steward Editor. Use --admin <email> only for
the small number of users who should receive the admin-access
Greenpill Operator role.
Scope steward editing to assigned chapters and guilds with a local TSV file:
bun run directus:content-access -- assign --input /path/to/content-access.tsvThe TSV columns are email, kind, and slug, where kind is chapter or
guild. Greenpill Steward Editor users can read published operational
content, edit only assigned draft/pending chapter or guild content, and create
chapter initiatives or guild projects only under assigned parent records.
Publishing remains reserved for trusted publishers and operators.
After changing roles, permissions, assignment data, or Directus metadata, run a temporary steward-session smoke test:
bun run directus:steward:smokeThe smoke test creates a temporary Steward Editor user, assigns it to one chapter and one guild, verifies assigned create/update behavior and forbidden unassigned creates through that user's token, then removes the temporary records.
If Docker Desktop is installed but docker is not on your shell PATH, the local
Docker scripts fall back to Docker Desktop's bundled CLI path on macOS. If
OrbStack is installed, the same wrapper uses OrbStack's Docker/Compose binaries
and socket.
Run Fly commands from the repo root so the workspace lockfile and package manifests are in Docker context.
fly config validate --config packages/agent/fly.toml
fly deploy --config packages/agent/fly.tomlThe agent Dockerfile is packages/agent/Dockerfile. The production database direction is Fly Managed Postgres attached to the Fly agent app so DATABASE_URL is available only as a private Fly secret.
The Directus admin app is network-admin and deploys from packages/admin:
fly deploy --config fly.toml packages/adminAttach the Fly Postgres connection to Directus as DB_CONNECTION_STRING, not as
DATABASE_URL, because Directus reads DB_CONNECTION_STRING for PostgreSQL
connection-string configuration.
Do not expose DATABASE_URL or direct database credentials to the public website deploy, Keystatic content, generated JSON, browser bundles, or any future Vercel project.
bun run test:agent
bun run test:chapter-impact
bun run test:content
bun run test:map-nodes
bun run test:plans
bun run plans:validate
bun run buildThe contract tests verify that agent routes, chapter impact payloads, public operational snapshots, public map payloads, SQL projections, and plan hubs preserve the public/private boundary.
Private map/member node intake must not expose emails, raw notes, IP addresses, user agents, spam metadata, steward review notes, pending submissions, or raw upstream EAS/Green Goods work and media feedback.
Public website data should stay static and public-safe. Directus-authenticated
operational edits must pass through the content schema, published views, and
shared snapshot guard before reaching the public website. Private intake, impact
cache state, migrations, and future authenticated operations belong behind the
Fly agent/workspace boundary.