> ## Documentation Index
> Fetch the complete documentation index at: https://docs.higherfrequency.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> 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...

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:

| To find | Look at |
| - | - |
| The backend modules | `ls services/core-service/src/modules/` |
| The API surface | Swagger UI at `/api`, or the `*.controller.ts` decorators |
| The data model | `services/prisma/schema.prisma` |
| Settings validated at boot | `services/core-service/src/common/config/env.validation.ts` |
| Paths served without `/api` | `GLOBAL_PREFIX_EXCLUDE` in `src/common/config/global-prefix.config.ts` |
| Rate-limit tiers | `src/common/throttler/throttle.constants.ts` |

## 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.

```
                 ┌────────────────────────── core-service (NestJS) ──────────────────────────┐
 Browser apps ──►│ /api/*   guards ─► controller ─► service ─► Prisma ─► PostgreSQL         │
 Shoppers     ──►│ /go/*    click redirect (public)                                           │
 Partners     ──►│ /partner/* HMAC pushes, bearer reads                                       │
 Shopify      ──►│ /api/shopify/webhooks/*  (signed)                                          │
 Stripe       ──►│ /api/payments/webhook/stripe (signed)                                      │
                 │                                                                            │
                 │ background workers: escrow release, order recovery, settlement delivery,  │
                 │ first-touch enrichment, risk checks, coupon expiry                         │
                 └──────────────┬─────────────────────────────────┬──────────────────────────┘
                                │ writes                          │ reads
                                ▼                                 ▼
                         EVM registry contract  ──events──►  subgraph indexer
```

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

| Recorded on-chain | Kept off-chain |
| - | - |
| Campaigns and their member creators | Funding, balances, fees and payouts |
| A click that led to a sale (`recordUserMovement`) | Click records for every click |
| Conversion, return, partial return, chargeback | Escrow holds and the double-entry ledger |
| Settlement (the commission has vested) | Store connections, products, orders |

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](https://docs.higherfrequency.io/developers/smart-contracts.md).

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](https://docs.higherfrequency.io/developers/attribution-engine.md).
* **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.

| What you see | What it really is |
| - | - |
| `JwtAuthGuard`, `@ApiBearerAuth('bearer')` | The guard verifies Clerk session tokens through `@clerk/backend`; the decorator declares the bearer scheme in Swagger. |
| `user_wallet_address` / `derivedAddress` on click and interaction rows | Not a wallet. It is a deterministic hash of the click id, shaped like an address so the contract can key on it. No key pair exists. |
| `Interaction` unique on `(campaignId, userWalletAddress)` | On the order path the "wallet" is the click hash, so this is effectively one row per click, not one per user. |
| `ShopifyStore`, `ShopifyOrder`, `shopify_orders` | Hold stores and orders for **every** provider. The `provider` column says which. |
| The registry NatSpec "first-click attribution" and the return value `isFirstClick` | The platform is last-click. `recordUserMovement` records one movement per click id and always returns `true`. |
| Order status `SETTLED` | A database status. On-chain, "settled" means the commission **vested**, not that anyone was paid. |
| A campaign's on-chain `returnWindow` | Only gates on-chain `registerReturn`. The escrow hold that protects money is a separate clock. |
| `ExperienceRegistry` | Dead. No contract, no ABI. Old scripts and comments that mention it are debris. |
| `Interaction.status = pending` | Nothing writes it. Code that filters on it sees an empty set. |

## 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).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.