Skip to main content
This page records what you cannot learn quickly by reading the code: the shape of the system, the decisions behind it, and the places where a name in the code says something different from what the code does. It deliberately does not list routes, modules, settings or contract addresses. Those change most weeks. Each one is a command away:

Shape of the system

One application service. services/core-service is a NestJS application and it is the entire backend. It serves the application API, the public /go click redirect, the partner endpoints, webhooks from Shopify and Stripe, and a Socket.IO gateway for real-time events. A single PostgreSQL database sits behind it, accessed through Prisma. The repository is a pnpm workspace with two packages: services/core-service and subgraph. Solidity contracts live at the repository root under contracts/ and are built with Hardhat.
There used to be a second service for ad serving and tracking. It was merged into the core service and retired. Its only surviving job, the /go redirect, now runs in-process. If you meet references to an “ad-service”, widgets, embed tokens or /track/* endpoints in older material, they describe a system that no longer exists.

The request path

  1. Prefix. Every route is under /api except health probes, the /go redirect and the /partner/* wire, which are served at the root.
  2. Validation. A global ValidationPipe runs with whitelist, transform and forbidNonWhitelisted. Unknown body or query fields are rejected with 400, not silently dropped.
  3. Rate limiting. One global throttler applies a generous per-user limit (per IP for anonymous callers). Sensitive routes override it with stricter tiers: on-chain writes, live chain reads, auth and wallet actions, telemetry sinks, and the partner read routes each have their own tier.
  4. Authentication. A guard verifies the Clerk session token (see below), then role, ownership and account-status guards run. Machine callers use HMAC or bearer guards instead; there are no API keys for users.
  5. Controller, then service, then Prisma. About a third of the modules add a repository layer between service and Prisma.
Real-time updates go out over Socket.IO. Clients authenticate the socket by sending the same Clerk token in the handshake (auth.token).

Authentication and tenants

Users sign in with Clerk. The backend verifies every session token through one verifier, which is used by the HTTP guard, the rate limiter and the WebSocket handshake alike. The platform is multi-tenant. A tenant is a partitioned marketplace with its own Clerk instance, allowed frontend origins, join policy and optional fee. The token’s issuer decides the tenant, and a user’s tenant never changes after registration. Campaign reads are scoped to the caller’s tenant on the server; a campaign from another tenant returns 404, exactly like one that does not exist. Partners are onboarded as tenants, not as forks of the code. Roles are ADVERTISER, PUBLISHER, ADMIN and SUPER_ADMIN. Only the first two can self-register, and a user holds exactly one role.

On-chain and off-chain

The contract is an audit trail of attribution. It never holds or moves money. Funds live in Stripe and in the ledger tables. Nothing is written on-chain when a shopper clicks; the chain is written only after an order has been attributed. Details in Smart contracts. After a transaction, the backend can read derived state from a subgraph that indexes the registry’s events.

Design decisions worth knowing

  • Attribution is last-click. The newest eligible click wins. This is a product decision, enforced in the theme embed and in the order resolver. See Attribution engine.
  • Fail closed on money. When evidence is incomplete or contradictory (a click from another store, a coupon that does not resolve, a candidate list that is too large), the order is left unpaid with a reason rather than guessed.
  • Ack fast, process later. Webhooks and partner pushes are persisted and acknowledged quickly. Attribution, chain writes and money movement happen asynchronously, with pollers that retry stuck work.
  • Idempotency everywhere. The order inbox de-duplicates on event id. Every ledger entry carries a unique idempotency key. Repeated deliveries are absorbed, never double-applied.
  • Append-only money. Ledger entries are never updated or deleted. Corrections are new entries.
  • Provider-neutral core. Shopify-specific code sits behind a provider interface so other commerce platforms reuse the same attribution and money engine.
  • Stores are soft-deleted. Store rows are never hard-deleted, so history and money remain traceable.

Names that lie

These are the traps that cost new engineers the most time.

Keeping this page honest

Write only what code cannot tell you: decisions, reasons and traps. If an answer is available from ls, the schema or a controller, link to it instead. An unverified claim is worse than a gap.

Authentication

Clerk is the authentication system. UAM and Pebble use separate Clerk instances; when both are configured, TokenVerifierService routes by issuer before verifying the signature and configured claims. AuthUserResolver scopes database matches to the token’s tenant. UAM-1065’s AUTH_GLOBAL_JWT_GUARD_ENABLED startup switch defaults to false. When enabled, the global guard runs JwtAuthGuard on every HTTP route except handlers or controllers marked @Public(). Explicit route guards remain active when the switch is off, and public service routes retain their own signature guards. After Clerk sign-in and email verification, registration stores the native Clerk subject and creates the advertiser/publisher wallet and party. Existing imported identities can resolve through external_id or a tenant-scoped verified-email match. HTTP relinking compares the previous identity and journals the update in one transaction; WebSocket resolution is read-only. History: Migrated from Auth0 in UAM-739 (2026-07); retired in UAM-1064 (2026-11).