Skip to main content
The GitHub template (apps/github-clone, name github) is an API-only clone of the GitHub REST API: a NestJS backend on Postgres that reproduces GitHub’s core repo-collaboration surface — users/orgs, repositories, issues, pull requests, comments, labels, milestones, branches, and commits — using real GitHub conventions: bare resource paths (/repos/{owner}/{repo}/…, /rate_limit), bare-JSON bodies (no wrapper), page+per_page pagination via an RFC-5988 Link header, integer id + node_id on every object, a {message, documentation_url} error envelope, and a GitHub-style rate limiter. It’s faithful enough at the URL + wire-format level that an agent built for GitHub can operate it.
asymmetric spin github

Routing — read this first

Real GitHub serves its REST routes at the root (/repos/..., /user, /rate_limit), with no /api prefix. So the backend uses no global prefix: data-plane controllers bind bare GitHub paths, while the clone’s own control-plane routes keep an explicit /api segment so they never collide with GitHub’s namespace.
PathSurface
/user, /users/…, /orgs/…, /repos/…, /rate_limitGitHub data plane (bare, faithful)
/api/healthinfra health probe
/api/auth/signup · login · refreshJWT web-auth for the clone
/api/user/tokens (+ POST, DELETE /:id)ghp_ personal-access-token management

What’s inside

PieceNotes
backend/NestJS REST API, JWT (access + refresh) + GitHub-style ghp_ personal access tokens, Postgres via pg. Health at /api/health, Swagger at /api/docs.
supabase/migrations/001_initial_schema.sql — the schema (13 tables), run on spin and replayed on reset.
seeds/acme.sql — the bundled deterministic fixture (acme org + acme/hello-world).

Modes

ModeServicesUse it for
api (default)backendAgents that talk to the REST API.
This template is API-only — there is no frontend.

Seeding

asymmetric spin github --seed acme           # deterministic fixture on create
asymmetric seed github-a1b2 --ai             # realistic data via the REST API
AI seeding generates users, repositories, and issues (the template’s declared entities) through real signup/repo/issue calls. See Seeding.

Tokens and auth

Every data route is guarded by default (a global AuthGuard); @Public() opts out (/api/auth/*, /api/health). The clone authenticates two ways, both as Authorization: Bearer … (the ghp_ path also accepts token ghp_…):
  • a JWT access token from POST /api/auth/signup / login / refresh (the web path), and
  • a GitHub-shaped personal access token (ghp_…) minted at POST /api/user/tokens. Only sha256(token) is stored; the plaintext is shown once at creation.
B=http://127.0.0.1:3006
TOK=$(curl -s $B/api/auth/signup -H 'content-type: application/json' \
  -d '{"login":"you","email":"you@acme.test","password":"password123"}' | jq -r .accessToken)
curl -s $B/user -H "authorization: Bearer $TOK"
Seeded users share the password password123 (e.g. octocat@acme.test).
A ghp_ token authenticates but its scopes are not enforced — any valid token can call anything. Scope enforcement, GitHub Apps / OAuth installs, and fine-grained tokens are out of scope for this slice. Reads currently require a token (real GitHub serves public GETs anonymously). Clones bind to localhost by default, so a token is not reachable off your machine unless you --expose.

API tour

B=http://127.0.0.1:3006
H="authorization: Bearer $TOK"
# Read the seeded org + repo (bare GitHub paths)
curl -s $B/orgs/acme -H "$H"
curl -s $B/repos/acme/hello-world -H "$H"
# List issues (open by default) with GitHub-style pagination headers
curl -si "$B/repos/acme/hello-world/issues?per_page=2" -H "$H" | grep -i '^link:'
# Open an issue (allocates the next per-repo number, shared with PRs)
curl -s -X POST $B/repos/acme/hello-world/issues -H "$H" \
  -H 'content-type: application/json' \
  -d '{"title":"New bug","labels":["bug"]}' | jq '{number,id,node_id,title}'
# Merge a pull request
curl -s -X PUT $B/repos/acme/hello-world/pulls/4/merge -H "$H"
# Check your rate-limit budget
curl -s $B/rate_limit -H "$H" | jq '.rate'

Pagination

List endpoints take page (1-based, default 1) and per_page (default 30, max 100), return a plain JSON array (no wrapper), and set an RFC-5988 Link header (rel=next/prev/first/last), CORS-exposed. Like GitHub, there is no X-Total-Count header — the Link header is the only pagination signal.

Rate limiting

A global rate limiter mirrors GitHub’s: 5,000 requests/hr authenticated, 60/hr unauthenticated (per-IP), on a fixed 1-hour window. Every response carries x-ratelimit-limit / -remaining / -used / -reset / -resource plus x-github-api-version and x-github-media-type. Exhaustion returns 403 with a retry-after header. GET /rate_limit reports the live budget as { resources: { core, search, graphql, … }, rate }.

API shape — read this before pointing an agent at it

Because GitHub is a REST API, the clone matches it at the URL and wire-format level for the implemented slice, not just the capability level:
  • bare resource paths (/repos/{owner}/{repo}/…, /rate_limit) — no /api prefix on the data plane,
  • addressing by login, {owner}/{repo} full name, per-repo integer number, integer comment_id, label name, and commit sha — never the internal UUID,
  • a numeric id plus a base64 node_id on every resource object,
  • bare-JSON bodies (no envelope) and the {message, documentation_url} error envelope; 422 validation failures carry errors[] of {resource, field, code},
  • Link-header pagination and the x-ratelimit-* budget headers described above.
Issues and pull requests share one per-repo number sequence, exactly like GitHub — numbers never collide within a repo. The template ships an API_PARITY.md that maps each implemented route to GitHub’s live REST docs. The implemented core spans users, orgs, repositories, issues (incl. lock/unlock), pull requests (incl. files, commits, is-merged, merge), issue comments, labels + issue-label assignment, milestones, branches, commits, and rate-limit — roughly 50 endpoints, with shape parity at 49/50 against GitHub’s live docs. Whole GitHub families outside the core collaboration surface (Actions, Checks, Search, Releases, Gists, Webhooks/Events, Git database internals, reviews, reactions, projects, GraphQL v4, and the official MCP server) are unimplemented by design — this is a vertical slice, not the full API. See apps/github-clone/API_PARITY.md for the per-route breakdown.
The verify command will fold live fidelity scoring into the CLI. Until then, API_PARITY.md and the /verify-api github workflow are how fidelity is tracked.

Inspect a running clone

asymmetric query github-a1b2 "select full_name, open_issues_count from repositories"
asymmetric db github-a1b2          # prints a psql shell command
asymmetric logs github-a1b2 -f     # stream the backend
The schema (users, repositories, issues, pull_requests, issue_comments, labels, milestones, branches, commits, …) comes straight from 001_initial_schema.sql — read it to know what’s queryable.