Architecture.

A high-level map of how PerkUp fits together — enough to orient yourself before you read code. It is deliberately general: the repo is the source of truth for how any one piece actually works. What follows is the shape of the system and the few concepts that hold across all of it.

The shape of the system

One product, a handful of moving parts. Members and admins use a single web app; it talks to a backend that runs mostly as Cloud Functions, with a legacy Go service still serving some integrations. Everything reads and writes the same datastore, and it all runs on GCP.

Client
Web app (SPA)
One React app — admin dashboard and the member experience. Served from object storage behind a CDN.
Backend · default
Cloud Functions
RPC services, webhooks, and triggers. The home for new backend work.
Backend · legacy
Go service
Older integrations (payments, HRIS, warehouses). Maintenance mode — still in the path, no new services.
Data
Firestore
The primary datastore. Both backends read and write the same documents.
Platform
GCP + external providers
Compute, storage, queues, and search on GCP; payments, HR, and commerce via third-party providers.

Simplified on purpose. Queues, search, storage, and a long tail of providers sit around this core — see Integrations for the provider map.

Key concepts

A few ideas hold across the whole system. Internalize these and most of the codebase will feel familiar.

  • One monorepo. The web app, both backends, shared packages, and the infrastructure-as-code all live in perkupapp/perkup-app. One checkout, one set of tooling.
  • One web app. Admins and members use the same SPA, gated by role — not two separate frontends.
  • Functions-first backend. New backend code is Cloud Functions in TypeScript. The Go service is in maintenance mode; we migrate toward functions, not away.
  • Contracts come first. Service and data shapes are defined as protobuf and generated into both Go and TypeScript, so the wire format is shared rather than hand-kept in sync. The generated code is checked in and CI fails on drift.
  • Firestore is the datastore. Document-oriented, shared by both backends. There is no separate relational database in the core path.
  • Everything runs on GCP, declared in code. Buckets, load balancing, queues, and pub/sub are defined in the pulumi/ stacks rather than clicked together in a console.

Where things live

When you are looking for the right place to put something, this is the orientation table. It is a starting point, not a rulebook — the directory's own AGENTS.md has the detail.

If you're working on…It lives in…
The web app — any admin or member screenapps/frontend/
New backend logic — an RPC service, webhook, or triggerfunctions/
An existing payments / HRIS / warehouse integrationbackend/app/ (Go, maintenance mode)
Something shared between app and backendpackages/ (@repo/*)
A service or data contractproto/
Cloud infrastructurepulumi/
Rule of thumb: new backend work is a TypeScript Cloud Function unless you're fixing something that already lives in the Go service. When in doubt, put it in the simplest place that can do the job and let review move it.

A reconstructed high-level overview compiled from the repo layout and AGENTS.md conventions on 2026-06-07 — orientation, not a ratified architecture spec. The code is the source of truth for how any piece actually works.