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.
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.
| Path | Surface |
|---|
/user, /users/…, /orgs/…, /repos/…, /rate_limit | GitHub data plane (bare, faithful) |
/api/health | infra health probe |
/api/auth/signup · login · refresh | JWT web-auth for the clone |
/api/user/tokens (+ POST, DELETE /:id) | ghp_ personal-access-token management |
What’s inside
| Piece | Notes |
|---|
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
| Mode | Services | Use it for |
|---|
api (default) | backend | Agents 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'
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.