GitHub template
A real GitHub REST API backend — repos, issues, pull requests, comments, labels, tokens, and rate limiting.
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.
hone env spin githubRouting — 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
hone env spin github --seed acme # deterministic fixture on create
hone env seed github-a1b2 --ai # realistic data via the REST APIAI 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 atPOST /api/user/tokens. Onlysha256(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).
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/apiprefix on the data plane, - addressing by
login,{owner}/{repo}full name, per-repo integernumber, integercomment_id, labelname, and commitsha— never the internal UUID, - a numeric
idplus a base64node_idon every resource object, - bare-JSON bodies (no envelope) and the
{message, documentation_url}error envelope; 422 validation failures carryerrors[]of{resource, field, code}, Link-header pagination and thex-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.
Inspect a running clone
hone env query github-a1b2 "select full_name, open_issues_count from repositories"
hone env db github-a1b2 # prints a psql shell command
hone env logs github-a1b2 -f # stream the backendThe 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.