Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

# Cancel in-progress runs for the same PR/branch when a new push lands.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
js:
name: JS (${{ matrix.package }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package:
- "." # root tooling package (lint-staged + simple-git-hooks + prettier)
- frontend
- mythos
- dbSchema
- sdk/sdk-typescript
- playground/merchant-express
- playground/merchant-fastify
- playground/merchant-hono
- playground/merchant-nextjs
- scripts/test-transaction
defaults:
run:
working-directory: ${{ matrix.package }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

# Corepack reads each package.json's `packageManager` pin and uses
# exactly that pnpm version, regardless of any globally-installed pnpm.
- name: Enable Corepack
run: corepack enable

# The load-bearing line of this whole workflow: refuses to mutate the
# lockfile and exits non-zero if package.json drifted from it. This is
# what catches PRs that would otherwise silently rewrite the lockfile.
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Format check
run: pnpm format:check

# --if-present skips silently for packages without a lint/build script.
- name: Lint
run: pnpm run --if-present lint

- name: Build
run: pnpm run --if-present build

go:
name: Go
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.25"
Comment on lines +72 to +74
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Go module cache will miss — cache-dependency-path needs to point to backend/go.sum

actions/setup-go@v5 is a uses: step and is not subject to defaults.run.working-directory. It searches for go.sum at the repository root; backend/go.sum is invisible to it. Every run will cache-miss and re-download modules.

⚡ Proposed fix
      - uses: actions/setup-go@v5
        with:
          go-version: "1.25"
+         cache-dependency-path: backend/go.sum
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 71 - 73, The Go module cache misses
because actions/setup-go@v5 looks for go.sum at the repository root; update the
actions/setup-go@v5 step to include the cache-dependency-path input pointing to
backend/go.sum (i.e., add cache-dependency-path: backend/go.sum alongside
go-version) so the action can find and use the correct go.sum for caching.


- name: gofmt
run: |
out=$(gofmt -l .)
if [ -n "$out" ]; then
echo "Files need gofmt:"
echo "$out"
exit 1
fi

- name: go vet
run: go vet ./...

- name: go build
run: go build ./...
Comment on lines +85 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Go CI job runs no tests — go test ./... is absent

The job only verifies formatting, vet, and that the binary compiles. Test regressions in backend/ won't be caught until deployment.

🧪 Proposed addition
      - name: go build
        run: go build ./...
+
+     - name: go test
+       run: go test ./...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: go vet
run: go vet ./...
- name: go build
run: go build ./...
- name: go vet
run: go vet ./...
- name: go build
run: go build ./...
- name: go test
run: go test ./...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 84 - 88, The CI job currently runs the
"go vet" and "go build" steps but does not run tests; add a new step (e.g.,
name: "go test") between or after the existing "go vet" and "go build" steps
that executes "go test ./..." (or "go test ./... -v") so unit tests across the
repository (including backend/) are executed and failures break the workflow;
reference the existing step names "go vet" and "go build" to locate where to
insert the new "go test" step.

50 changes: 50 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Dependencies
node_modules/

# Build outputs
.next/
dist/
build/
out/
.turbo/
coverage/

# Lockfiles (managed by package managers)
pnpm-lock.yaml
package-lock.json
yarn.lock

# Minified
*.min.js
*.min.css

# Non-JS/TS roots — formatted by their own tools (gofmt, ruff/black, etc.)
backend/
dbSchema/drizzle/
sdk/python/
playground/main.py
playground/pyproject.toml
playground/merchant-fastapi/
playground/merchant-jokes/
playground/merchant_server/
playground/e2e_test/

# Python virtualenvs and cache anywhere in the tree
**/.venv/
**/__pycache__/
**/*.dist-info/

# Generated / vendored
**/.next/**
**/dist/**

# Existing CI workflows (out of this PR's reformat scope; touch in a dedicated cleanup)
.github/workflows/deploy-sdks.yml
.github/workflows/publish-python-sdk.yml
.github/workflows/publish-ts-sdk.yml

# Working/plan docs not committed to the repo
CRITICAL_SECURITY_ISSUES.md
DOC_INCONSISTENCIES.md
H8_PART_A_PLAN.md
TOOLING_HYGIENE_PLAN.md
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "all",
"arrowParens": "always"
}
38 changes: 23 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,28 @@ For protocol details see [Sangria-Overview.md](Sangria-Overview.md). For archite

## Repository Map

| Directory | What | Stack |
|---|---|---|
| `backend/` | Orchestration API — accounts, payments, settlement, withdrawals | Go 1.25, Fiber v3, pgx/pgxpool |
| `dbSchema/` | Database schema — single source of truth | Drizzle ORM (TypeScript), PostgreSQL |
| `frontend/` | Docs site + merchant dashboard | Next.js 16, React 19, Tailwind 4 |
| `sdk/sdk-typescript/` | TypeScript merchant SDK (`@sangria-sdk/core`) | TypeScript, adapters for Express/Fastify/Hono |
| `sdk/python/` | Python merchant SDK (`sangria-core`) | Python 3.10+, httpx, FastAPI adapter |
| `playground/` | Example merchant servers + e2e test client | Express, Fastify, Hono, FastAPI, uv |
| `mythos/` | Internal admin dashboard | Next.js 16, WorkOS AuthKit, port 3001 |
| Directory | What | Stack |
| --------------------- | --------------------------------------------------------------- | --------------------------------------------- |
| `backend/` | Orchestration API — accounts, payments, settlement, withdrawals | Go 1.25, Fiber v3, pgx/pgxpool |
| `dbSchema/` | Database schema — single source of truth | Drizzle ORM (TypeScript), PostgreSQL |
| `frontend/` | Docs site + merchant dashboard | Next.js 16, React 19, Tailwind 4 |
| `sdk/sdk-typescript/` | TypeScript merchant SDK (`@sangria-sdk/core`) | TypeScript, adapters for Express/Fastify/Hono |
| `sdk/python/` | Python merchant SDK (`sangria-core`) | Python 3.10+, httpx, FastAPI adapter |
| `playground/` | Example merchant servers + e2e test client | Express, Fastify, Hono, FastAPI, uv |
| `mythos/` | Internal admin dashboard | Next.js 16, WorkOS AuthKit, port 3001 |

Per-package build/test commands are in each package's own CLAUDE.md where one exists. See `backend/CLAUDE.md`, `dbSchema/CLAUDE.md`, `frontend/CLAUDE.md`, `sdk/sdk-typescript/CLAUDE.md`.

## Dev vs Prod Environments

Sangria runs two strictly separated environments — different Postgres databases, different CDP/facilitator endpoints, different WorkOS tenants, different chain contexts (Base Sepolia for dev, Base mainnet for prod).

| Scope | Dev convention | Prod convention |
|---|---|---|
| Backend | Loads `.env` (copy from `.env.example`) | Railway runtime env vars |
| dbSchema | `pnpm push:dev` (loads `.env.dev`) | `pnpm push:prd` (loads `.env.prd`) |
| Frontend | `.env` / `.env.local` | Railway runtime env vars |
| Playground | `.env` with CDP testnet keys, Base Sepolia | Never runs against prod |
| Scope | Dev convention | Prod convention |
| ---------- | ------------------------------------------ | ---------------------------------- |
| Backend | Loads `.env` (copy from `.env.example`) | Railway runtime env vars |
| dbSchema | `pnpm push:dev` (loads `.env.dev`) | `pnpm push:prd` (loads `.env.prd`) |
| Frontend | `.env` / `.env.local` | Railway runtime env vars |
| Playground | `.env` with CDP testnet keys, Base Sepolia | Never runs against prod |

**Default is always dev.** `go run .`, `pnpm dev`, and all local commands hit the dev environment. Production configs are for CI/CD and production runtimes only.

Expand All @@ -53,33 +53,39 @@ Use these terms consistently:
## Non-Negotiable Principles

### Schema

- **Schema lives in Drizzle.** Any schema change starts in `dbSchema/schema.ts`. Go code is a consumer, never the author. Never hand-write SQL DDL.
- **Enforce correctness at the database layer** (NOT NULL, unique, FK, CHECK). Never rely on caller discipline.
- Schema conventions (column types, naming, FK rules) live in `dbSchema/CLAUDE.md`.

### Money & Ledger

- **Double-entry bookkeeping for all USDC->USD flows.** Every movement debits and credits named accounts. The ledger is the source of truth for balances.
- **x402 settle is NOT HTTP-idempotent.** Persist intent before calling. Treat ambiguous HTTP responses as UNRESOLVED (not failed) and reconcile against on-chain state. Never release a fiat payout on HTTP 200 alone.
- **EIP-3009 nonces give on-chain idempotency but do not solve HTTP-layer ambiguity.** Don't conflate the two.
- Amount representation is defined in § Product Vocabulary (microunits).

### Code

- **Atomic admin checks.** Permission checks and mutations should be in the same SQL query to prevent TOCTOU races.
- **Email normalization.** Always `strings.TrimSpace(strings.ToLower(email))` before storing or matching.
- **Sentinel errors.** Use package-level `var Err... = errors.New(...)` for typed error handling.

### Security

- **CSRF Protection is automatic.** Frontend components use standard `internalFetch()` calls — never manual CSRF token handling. The fetch wrapper (`lib/fetch.ts`) automatically injects tokens. Backend validates via `auth.CSRFMiddleware()`.
- **Use secure fetch wrapper.** Import `{ internalFetch } from "@/lib/fetch"` instead of global `fetch` for automatic CSRF protection on state-changing requests.

### SDK

- **SDK surface is a product.** Breaking changes to `@sangria-sdk/core` or `sangria-core` need explicit justification.
- Match idioms of each host framework (Express middleware vs Fastify plugin vs FastAPI dependency) rather than forcing a single abstraction.
- Keep TypeScript and Python SDK behavior in lockstep. If you add a feature to one, either add it to the other or explicitly document why it's language-specific.
- When changing SDK behavior, update the relevant playground examples.
- **Bump SDK versions in `deployment/SDK_VERSIONS.md`** — it's the single source of truth. CI auto-bumps the patch version if you forget, but explicit edits communicate intent (patch = fix, minor = feature, major = breaking per semver). Never hand-edit `sdk/sdk-typescript/package.json#version` or `sdk/python/pyproject.toml#version` — CI overwrites them at publish time. See `deployment/DEPLOYMENT.md` for the full flow.

### Process

- Ask clarifying questions before architectural changes.
- Prefer principled reasoning over "what to change" — explain the why.
- Match existing patterns in the codebase; flag inconsistencies rather than silently homogenizing.
Expand All @@ -105,9 +111,11 @@ Applies to both `frontend/` (merchant portal) and `mythos/` (admin dashboard)
### Proxy routes (`app/api/**/route.ts`)

- **URL-encode every dynamic segment** before interpolating into the backend path:

```ts
return proxyToBackend("POST", `/admin/withdrawals/${encodeURIComponent(id)}/approve`, { body });
```

Raw `${id}` lets a caller inject `/` or `..` to reach a different backend route with the authenticated bearer token attached.

- **CSRF Protection**: Pass the request object to `proxyToBackend()` for automatic CSRF token extraction:
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ make down # Stop all services

Requires Docker. Services: backend on `:8080`, frontend on `:3000`, mythos (admin) on `:3001`.

### One-time setup for new contributors

The repo pins `pnpm@10.15.1` via the `packageManager` field in every `package.json`. Use Corepack (ships with Node 20+) so every developer runs the exact same pnpm version regardless of what's globally installed:

```bash
corepack enable # one-time, makes the `pnpm` binary respect packageManager pins
pnpm install # at repo root — installs pre-commit tooling and sets up git hooks
```

The root `pnpm install` configures a pre-commit hook (`simple-git-hooks` + `lint-staged`) that runs Prettier on staged files. Skip with `SKIP_SIMPLE_GIT_HOOKS=1 git commit ...` if needed. CI also runs `pnpm install --frozen-lockfile`, `pnpm format:check`, and `pnpm build` (where present) on every PR — so any lockfile drift or unformatted code fails before review.

---

## Quick Start
Expand All @@ -42,7 +53,7 @@ app.get(
fixedPrice(sangria, { price: 0.01, description: "Premium content" }),
(req, res) => {
res.json({ data: "premium content", tx: req.sangria?.transaction });
}
},
);

app.listen(3000);
Expand Down
Binary file removed backend/sangria-backend
Binary file not shown.
2 changes: 1 addition & 1 deletion dbSchema/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ pnpm studio:prd # Open Drizzle Studio browser (prod)
- Some account rows have a nullable org-scope column — system-level rows leave it `NULL`, org-scoped rows set it. Check the schema before assuming non-null.
- WorkOS IDs use TEXT columns, not UUID, to avoid cast issues with external identifiers.
- The migration directory (`drizzle/`) has a single initial migration. Subsequent schema changes are applied via `drizzle-kit push` rather than generated migrations.
- **Do not use `BigInt(0)` as a default on `bigint` columns.** drizzle-kit 0.31.10 can't `JSON.stringify` a BigInt during its schema-diff phase and `pnpm push:dev`/`push:prd` will crash with `TypeError: Do not know how to serialize a BigInt`. Use `` .default(sql`0`) `` instead — same on-disk behaviour (DEFAULT 0 coerced to bigint by Postgres), no serialization bug. Auto-fixers occasionally "correct" it back to `BigInt(0)` because that looks more natural — don't accept that change.
- **Do not use `BigInt(0)` as a default on `bigint` columns.** drizzle-kit 0.31.10 can't `JSON.stringify` a BigInt during its schema-diff phase and `pnpm push:dev`/`push:prd` will crash with `TypeError: Do not know how to serialize a BigInt`. Use ``.default(sql`0`)`` instead — same on-disk behaviour (DEFAULT 0 coerced to bigint by Postgres), no serialization bug. Auto-fixers occasionally "correct" it back to `BigInt(0)` because that looks more natural — don't accept that change.
Loading