Skip to content

Latest commit

 

History

1,014 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Overflow

Overflow is a cooperative ledger for open-source work. A repository sponsor offers work, an outside contributor closes it through GitHub, and Overflow records a settled credit transfer with auditable proof.

Overflow is already running at https://overflow.nitjsefni.eu. Pointing you at that instance is what this repository is for. You do not need to deploy anything to use Overflow — sign in there and join the ledger that already exists. The setup instructions further down build a development environment for changing Overflow itself; they are not the way to use it.

Nitjsefnie/Overflow is itself registered in that instance, and the issues in this tracker are materialized there, so the mechanism described below can be watched working on this repository itself.

Join the running instance

  1. Sign in. Open https://overflow.nitjsefni.eu and choose Sign in with GitHub. That is the whole account setup — there is nothing to install and nothing to configure.
  2. Register a repository, catalogs and all, on one form. Register a repository takes the repository and both of its catalogs and submits them together. Bring a public repository you administer. Registration writes to it: Overflow creates the catalog labels there and installs its webhook. What the ledger records is the reference for what a catalog has to contain. Catalogs can be changed later — on the same page, or over the API — and a change never re-prices work that has already settled.
  3. Offer work, then settle it. Apply an opening label when you file an issue. After the closing pull request's final commit and before you merge it, apply an actual-catalog label and post a comment naming that label — as the sponsor; nobody else's labels or comments price your repository's work, and a comment edited after the merge window closes no longer counts. Those are the labels Overflow created for you in step 2. What the ledger records states the evidence each label has to satisfy, and Scoring and calibration says what it is worth.
  4. Read the ledger. A signed-in member gets Ledger, Issues, Settlements, Register a repository, Calibration and Rules.

Closing work needs no repository of your own. Take an issue in a repository that is already registered; the sections that follow are the terms the credit settles on, including what happens when you have not signed in yet.

Programmatic repository registration

Members can register repositories with an Overflow-issued API token. Account registration still happens manually in a browser through Sign in with GitHub; after that, repositories can be registered over the API. The existing web form remains unchanged and available — programmatic registration is an additional way to submit the same repository and catalogs.

Get or replace a token

Sign in, open Register a repository (/repositories/new), and use Generate token in the Overflow API token panel. Copy the token when it appears: it is shown only at generation and cannot be redisplayed after leaving or reloading the page. The server stores only its hash, so it cannot recover the plaintext.

Each account has at most one active token. Regenerate token issues a new token and invalidates the previous one in the same step. Use regeneration if you lose the token or it leaks, and replace the credential in your scripts. Keep the token private; do not commit it.

The panel calls POST /api/tokens with the signed-in browser session cookie and no request body. An API token alone cannot mint or regenerate a token. Because the session cookie is the only credential, the endpoint is same-origin only: the request must carry an Origin header equal to the origin of APP_URL (its scheme, host and port; any path is ignored), and it must either send no body or declare Content-Type: application/json. Success is HTTP 201 with { "token": "<new-token>", "createdAt": "<ISO-8601 timestamp>" }. Failures use { "error": { "code": "...", "message": "..." } }:

HTTP Code Exact message Meaning / next step
401 UNAUTHENTICATED Sign in is required. Sign in through GitHub in the browser.
403 FORBIDDEN The request origin is not allowed. The request carried no Origin header or one that is not the origin of APP_URL. Mint the token from the Overflow page in the browser.
415 UNSUPPORTED_MEDIA_TYPE The request must use the application/json content type. The request declared a Content-Type that is not application/json. Send no Content-Type at all, or send application/json.
500 MISCONFIGURED The server is not configured to accept this request. The deployment's APP_URL is missing or malformed, so it cannot recognize its own origin. Fix the server configuration; nothing about the request will help.
502 UPSTREAM_FAILURE Unable to issue an API token. Session lookup or token storage failed; retry when the service recovers.

Submit a repository

Send POST /api/repositories with Authorization: Bearer <token> and Content-Type: application/json. Use the Overflow-issued token; registration uses the account's stored GitHub OAuth credential for its GitHub operations. The repository must be public, and that account must have GitHub administrator permission for it. Registration creates the catalog labels and installs a webhook.

A bearer-token request is exempt from the origin check — a script is not a browser and sends no Origin header — but it is not exempt from the content type: Content-Type: application/json is required either way. The same endpoint reached with a browser session cookie instead of a bearer token is same-origin only, so its Origin must equal the origin of APP_URL.

The JSON body contains exactly these required fields. Extra fields, including extra fields inside label objects, are rejected.

Field Type and requirements
repositoryUrl String: one owner/name or canonical GitHub repository URL.
openingName Nonblank string: opening catalog display name.
actualName Nonblank string: actual catalog display name.
openingLabels Nonempty array of { "label": string, "comparisonPoints": number, "reservePoints": number }. Both point values must be integers from 1 through 10.
actualLabels Array of { "label": string, "points": number } with exactly ten entries, covering every integer from 1 through 10 exactly once.

All label text must be nonblank and unique across both catalogs. Opening labels can use any names and need not cover every point. Do not shorten the actual catalog to S/M/L: missing points cause rejection.

Replace <overflow-origin> with the origin of the Overflow instance you use (scheme and authority, without a trailing slash), <your-token> with the token from its panel, and your-org/your-repository with a public repository you administer that is not already registered. Then run this complete example:

OVERFLOW_ORIGIN='<overflow-origin>'
OVERFLOW_API_TOKEN='<your-token>'

curl --include --request POST "${OVERFLOW_ORIGIN}/api/repositories" \
  --header "Authorization: Bearer ${OVERFLOW_API_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "repositoryUrl": "your-org/your-repository",
  "openingName": "Estimated scope",
  "actualName": "Delivered difficulty",
  "openingLabels": [
    { "label": "offered: small", "comparisonPoints": 2, "reservePoints": 2 },
    { "label": "offered: medium", "comparisonPoints": 5, "reservePoints": 5 },
    { "label": "offered: large", "comparisonPoints": 8, "reservePoints": 8 }
  ],
  "actualLabels": [
    { "label": "settled: 1", "points": 1 },
    { "label": "settled: 2", "points": 2 },
    { "label": "settled: 3", "points": 3 },
    { "label": "settled: 4", "points": 4 },
    { "label": "settled: 5", "points": 5 },
    { "label": "settled: 6", "points": 6 },
    { "label": "settled: 7", "points": 7 },
    { "label": "settled: 8", "points": 8 },
    { "label": "settled: 9", "points": 9 },
    { "label": "settled: 10", "points": 10 }
  ]
}
JSON

Registration responses

Success is HTTP 201. Example body (identifiers vary):

{
  "repository": {
    "id": "<repository-id>",
    "githubRepositoryId": 123456789,
    "ownerName": "your-org/your-repository",
    "sponsorId": "<account-id>",
    "visibility": "PUBLIC",
    "githubWebhookId": 987654321
  },
  "initialImportScheduled": true
}

initialImportScheduled reports whether the import of the work that already exists in the repository was queued, not whether it has finished: the import runs after the response, and the issues appear once it does. If it is false, the repository is still registered but nothing is queued; the periodic repair sweep brings its existing work in later, or arrange a reconciliation yourself. Registering the repository again is never the remedy.

Errors have { "error": { "code": "...", "message": "..." } }. Match the HTTP status and code, then use the message to distinguish causes:

HTTP Code Exact message Meaning / next step
400 INVALID_REQUEST Invalid repository registration request. Invalid JSON, missing or extra fields, or wrong field types. Correct the body.
400 INVALID_INPUT Submit one GitHub repository as owner/name or a canonical GitHub URL. Correct the repository reference.
400 INVALID_INPUT The repository is missing the difficulty labels <labels>. Create them on GitHub, then register again. The repository's existing labels do not include every label the submitted catalog names; <labels> is the backticked list of the missing ones. Create those labels on GitHub, then register again.
400 INVALID_INPUT Catalog validation message listed below. Correct the catalog names, labels, or points.
401 UNAUTHENTICATED The supplied API token was not accepted. The bearer credential has an invalid token format or is unknown (including a revoked token). Check the copied token or generate a replacement in the browser.
401 UNAUTHENTICATED Sign in is required. No recognized bearer credential and no signed-in session. Supply the bearer header or sign in.
401 GITHUB_CREDENTIALS GitHub rejected the authorization Overflow holds for this account (HTTP 401) while trying to <step>. To refresh the authorization, sign out of Overflow and sign in again with GitHub, then retry registration. GitHub rejected the stored GitHub authorization for the account (expired or revoked); the account's Overflow session is fine. Refresh the authorization by signing out and back in, then retry the registration.
403 FORBIDDEN The request origin is not allowed. A browser (session-cookie) request carried no Origin header or one that is not the origin of APP_URL. A bearer-token request never reaches this: its origin is not consulted.
403 FORBIDDEN The account is not eligible to register repositories. The account is banned or recalibrating. Resolve the account restriction; regenerating the token does not remove it.
403 FORBIDDEN Only public GitHub repositories can be registered. Choose a public repository.
403 FORBIDDEN GitHub administrator permission is required for the submitted repository. Use an account with administrator permission for that repository.
403 GITHUB_ACCESS GitHub refused to <step> (HTTP 403). GitHub answers 403 both when the Overflow OAuth application is not yet authorized and when it is temporarily limiting requests, and this response carries nothing that separates the two causes. Wait a minute and retry registration before changing anything. <cause> Review Overflow's authorization at https://github.com/settings/applications, then retry registration. GitHub refused a setup step without rate-limit evidence, so the answer cannot separate a missing Overflow OAuth authorization from a temporary limit; wait a minute and retry first. <step> is the lookup, label-read, or webhook-create step that died, and <cause> names the missing authorization — for an organization-owned repository, the owner approval the Overflow application needs there.
403 GITHUB_ACCESS GitHub answered 404 for the request to <step>. GitHub returns 404 rather than 403 when it will not reveal a resource, which can indicate missing authorization. The repository may also have been renamed, moved or deleted<since it was looked up>. <cause> Review Overflow's authorization at https://github.com/settings/applications, then retry registration. GitHub hid the resource the setup step asked for, which it does instead of refusing it; treat the step as unauthorized until checked. <since it was looked up> appears when the repository had already been looked up, and <cause> again names the missing authorization, with the organization-owned variant as above.
409 CONFLICT This GitHub repository is already registered. Use the existing registration.
409 CONFLICT The GitHub path <owner/name> is claimed by a different registration. The submitted repository is not registered, and it cannot be registered while another registration holds that path. The submitted repository has never been registered, but another registration holds its owner/name. Retrying repeats the same collision; the registration holding the path has to be resolved first.
409 CONFLICT The GitHub webhook created for the submitted repository collided with one a different registration already records. The submitted repository is not registered. Registering again requests a new webhook from GitHub, so retry once before treating this as stored state that has to be resolved. The submitted repository has never been registered. The collision is on the webhook id GitHub returned for the hook this attempt created, and registering again requests another webhook, so retry once first. Only a collision that repeats points at stored state that has to be resolved.
415 UNSUPPORTED_MEDIA_TYPE The request must use the application/json content type. The request declared a Content-Type that is not application/json. This applies to bearer-token requests too, and is answered before the token is looked up.
429 GITHUB_RATE_LIMITED GitHub rate-limited the request to <step> (HTTP <status>). Please retry registration later. GitHub limited a setup step. <status> is 429, or 403 carrying rate-limit evidence; GitHub may append a Retry after <N> seconds. sentence when it supplies a delay, and the message ends with the retry instruction either way. Wait out the delay if given, then retry.
500 MISCONFIGURED The server is not configured to accept this request. The deployment's APP_URL is missing or malformed. Only a browser request reaches this; a bearer-token request does not read APP_URL.
502 UPSTREAM_FAILURE Unable to initialize repository registration. Credential/session lookup or registration setup failed; check service configuration and account GitHub access before retrying.
502 UPSTREAM_FAILURE Unable to retrieve the submitted GitHub repository. GitHub repository lookup failed; check the reference, access, and GitHub availability.
502 UPSTREAM_FAILURE Unable to read the repository difficulty labels on GitHub. Reading the repository's existing labels failed after the repository itself was found; check GitHub access and availability.
502 UPSTREAM_FAILURE Unable to create the repository webhook on GitHub. Creating the webhook failed after the repository was found and its labels verified; check GitHub access and availability.
502 UPSTREAM_FAILURE Unable to save the repository registration. Database lookup or saving the registration failed; check service health before retrying.
503 ROLLBACK_INCOMPLETE The repository registration could not be saved, and the webhook Overflow created for it could not be deleted on GitHub. Nothing was registered; retry the registration, and a later successful registration or unregistration removes the abandoned webhook. The save failed and the compensating webhook deletion failed too, so a webhook Overflow created still exists on GitHub. Nothing was registered and nothing is lost by retrying; the recorded webhook is cleaned up by a later successful registration or unregistration.

Angle-bracketed text in the Exact message column is a value substituted at runtime.

Catalog validation returns one of these exact INVALID_INPUT messages:

  • Display names must not be empty.
  • At least one opening label is required.
  • Opening label text must not be empty.
  • Difficulty label text must be unique.
  • Opening point mappings must be integers from one through ten.
  • Actual label text must not be empty.
  • Difficulty label text must be unique across catalogs.
  • Actual point mappings must be integers from one through ten.
  • Actual point mappings must be unique.
  • Actual labels must cover points one through ten exactly once.

Authentication runs before body validation. A recognized bearer credential takes precedence over the browser cookie: a rejected token is not rescued by a valid session. An absent or malformed bearer header falls back to cookie authentication, which continues to serve the web form.

Changing a registered catalog

A repository's difficulty catalog is a versioned series, not a fixed choice. PATCH /api/repositories accepts the same body a registration takes and appends the submitted catalog as the repository's next catalog version; the browser form on the Register a repository page does the same. The version begins governing at the moment of the change, so:

  • closures whose evidence window closed before the change keep resolving at their recorded figures, and
  • closures whose evidence window closes after the change are priced by the new catalog.

A submission identical to the current catalog changes nothing and reports it. Errors have the same shape as the registration errors above; the change path answers CONFLICT for an unregistered repository, and FORBIDDEN for anyone but the repository's sponsor or an account that is not eligible to change repository catalogs.

Reading the ledger over the API

Each page a signed-in member reads has a GET endpoint answering the same JSON the page renders: the dashboard behind the Ledger page, the issues board, the settlement history, a settlement's proof, and calibration. They use the same Overflow-issued ovf_ tokens as registration, sent as Authorization: Bearer <token>; a browser can call them with its signed-in session cookie instead. The credential rules are the ones registration states: a recognized bearer credential takes precedence over the cookie, a rejected token is not rescued by a valid session, and an absent or malformed bearer header falls back to the cookie. Unlike the registration endpoints, these reads are answered without an origin check — a programmatic GET sends no Origin header at all, so guarding them would reject every script client. They take no request body.

Every response is scoped to the authenticated account: each endpoint queries with the credential's account id, and the settlement proof is answered only to a caller who is a party to that settlement.

The endpoints

Endpoint Parameters Success body (HTTP 200)
GET /api/dashboard None. The dashboard projection the Ledger page renders. Each entry of registeredRepositories carries reconciliationLastFailureAt as an ISO 8601 string, or null when there is no recorded failure.
GET /api/issues Query parameters repository, openingLabel, claimState, all optional. An array of the issue projections the board renders.
GET /api/settlements None. An array of the settlement-history rows the page renders.
GET /api/settlements/<id> Path parameter id. { "settlement": <settlement proof projection>, "corrections": <correction requests raised against the settlement, or null> }.
GET /api/calibration None. { "comparison": <calibration comparison>, "selfWork": <self-work calibration rows, or null> }.

The field lists of these projections are the server's compiled types, not a schema this document maintains; this section records the endpoints, their parameters, and their status behavior.

On /api/issues, a filter is applied only when the request names exactly one value for it — a parameter named more than once is left unset. claimState understands CLAIMED and ALL; anything unrecognized, including no value at all, reads the unclaimed board (OPEN).

Two responses degrade rather than fail. In the settlement proof, corrections is null when the correction history could not be read, and the settlement itself is still answered; in the calibration response, selfWork is null under the same terms and the comparison is still answered. The pages render the same degradation, so neither null is an API-only shape.

Both list reads are capped at the most recent 200 rows — the settlement history on /api/settlements and the selfWork list on /api/calibration — mirroring what the pages render, so a capped list is not distinguishable from complete history.

Read responses

Success is HTTP 200. Errors use the registration error envelope, { "error": { "code": "...", "message": "..." } }. Match the HTTP status and code, then use the message to distinguish causes:

HTTP Code Exact message Meaning / next step
401 UNAUTHENTICATED The supplied API token was not accepted. The bearer credential has an invalid token format or is unknown (including a revoked token). Check the copied token or generate a replacement in the browser.
401 UNAUTHENTICATED Sign in is required. No recognized bearer credential and no signed-in session. Supply the bearer header or sign in.
403 FORBIDDEN A member account is required. The credential resolved to an account that no longer exists: the member gate re-reads the account's role from the database at request time, so a session or token outliving its account is refused.
404 NOT_FOUND Settlement proof is not available. (GET /api/settlements/<id>) No settlement with this id, or the caller is not a party to it. Both are the same refusal.
502 UPSTREAM_FAILURE Unable to authorize the member request. Credential lookup or the role re-read failed; retry when the service recovers.
502 UPSTREAM_FAILURE Unable to load the eligible issues. (GET /api/issues) The read behind the endpoint failed; retry when the service recovers.
502 UPSTREAM_FAILURE Unable to load the settlement history. (GET /api/settlements) The read behind the endpoint failed; retry when the service recovers.
502 UPSTREAM_FAILURE Unable to load the settlement proof. (GET /api/settlements/<id>) The read behind the endpoint failed; retry when the service recovers.
502 UPSTREAM_FAILURE Unable to load the calibration comparison. (GET /api/calibration) The read behind the endpoint failed; retry when the service recovers.
502 UPSTREAM_FAILURE Unable to load the dashboard. (GET /api/dashboard) The read behind the endpoint failed; retry when the service recovers.

Calling Overflow from an agent harness

The API above is also exposed as MCP tools over a single endpoint, POST /api/mcp, so an agent harness can read the ledger and drive the moderation and correction flows without hand-rolling HTTP calls against the page endpoints.

Authentication uses the same Overflow-issued ovf_ tokens, sent as Authorization: Bearer <token>. Browser sessions work for the reads, but token auth is the intended credential: a cookie-authenticated write through MCP is refused, because the synthesized internal calls carry no Origin header for the same-origin guard to check.

The transport is stateless streamable HTTP — one JSON-RPC request per POST, and no session state between calls. initialize answers with protocol version 2025-06-18; a notification (a JSON-RPC request with no id) is answered with an empty HTTP 202.

The tools

Tool Purpose
issues_board List the eligible issues on the claim board, optionally filtered by repository, opening label or claim state.
settlements_list List the calling account's priced settlements.
settlement_get Fetch one settlement's proof by its id.
calibration_compare Fetch the calibration comparison for the calling account.
dashboard_summary Fetch the calling account's dashboard summary.
moderation_queue List the account audits currently open in the moderation queue.
audit_open Open an account audit over a calibration sample.
audit_decide Dismiss or substantiate an open account audit.
correction_open Request a correction to a priced settlement or calibration outcome.
correction_decide Grant or decline a settlement correction request.

Tool errors are not transport errors: the wrapped endpoint's { "error": { "code": "...", "message": "..." } } envelope comes back as the result's text content with isError: true, so read the text to tell a validation refusal from an upstream outage.

On issues_board, a filter argument that is not a string is dropped from the query rather than rejected, so an omitted argument and a malformed one read the same board.

What the ledger records

  • GitHub OAuth signs a member in at /api/auth/callback/github.
  • Repository registration is explicit and one at a time. The submitted owner/name or canonical https://github.com/owner/name URL must be a public repository, and the signed-in person must have GitHub administrator permission for it.
  • Each repository chooses its own opening catalog. S/M/L is allowed, but so are arbitrary labels such as moonlit ridge, risk: high, or anything else the repository understands. Each opening label carries comparison and reserve points from 1 through 10.
  • Every actual catalog has exactly one editable mapping for each point from 1 through 10. The labels are repository-defined; the point mapping is the common settlement scale.
  • The dashboard uses materialized ledger entries and balances. Available headroom is settled balance − reserve points for open issues assigned to outside contributors, and negative headroom remains visible. This release enforces no credit floor and exposes no floor configuration; optional group floors await a later idempotent assignment-enforcement design.

Closing-link evidence comes only from GitHub GraphQL closedByPullRequestsReferences. Opening difficulty is reconstructed from the earliest configured label that the repository sponsor applied before the first assignment. Settled difficulty requires exactly one active actual-catalog label, applied by the sponsor between the closing pull request's final commit and merge, plus a nonblank sponsor comment naming that label. Only the sponsor prices work; being the issue's author grants no pricing authority. Work completed by the sponsor is self-work calibration, not a settlement. Pull-request labels never price work.

A 15-minute tolerance applies to label and comment timing; the settlement window closes 15 minutes after merge. A rationale comment edited after that close does not count. The earliest qualifying comment at or after the standing label is used, including when a label is reapplied; if none exists, a comment up to 15 minutes before that label can count. Overflow retains the accepted event/comment identifiers and timestamps, the exact merge commit OID, and the diff fingerprint so every scoring input is reproducible.

Contributors and moderators are identified by their immutable GitHub account id; a GitHub login is displayed but never decides who is credited or who is a moderator.

Scoring and calibration

For an outside contributor, settled credits are:

credits = max(0, actual points − distinct review rounds)

There is no churn metric. Review rounds are the distinct changes-requested reviews submitted before merge, counted as they stood when the pull request merged: a review dismissed after the merge still counts, and one dismissed before the merge does not. A dismissal exactly at merge also leaves the round counted; no timing tolerance applies to reviews. A dismissed review counts only if its dismissal history establishes that it requested changes; missing history or an unknown previous state does not count.

Calibration compares paired self-work samples with outsider settlements; it does not measure activity retention. Self-work is useful calibration evidence, but it creates no ledger entry. If an outside contributor has not signed in yet, their completed work remains an unclaimed settlement until their GitHub identity is claimed.

Moderation is account-level and evidence-led:

audit → warn → recalibrate → ban

Open an audit only with the required paired samples, warn when the record supports it, require recalibration before reactivation, and ban only after confirmed patterns persist.

Running your own instance is not the way to use Overflow

Each instance keeps its own ledger. Balances, reserves, settlements, proof records and calibration history live in that instance's own PostgreSQL database, and nothing in this codebase moves them between deployments. A second instance therefore starts empty and stays private to itself: no registered repositories, no counterpart to settle with, and no credit that anyone else can see or honour. Signing in at https://overflow.nitjsefni.eu is what puts your work in a ledger other people are already reading.

Development setup

These steps stand up a local copy of the application against a local PostgreSQL database.

  1. Copy .env.example to .env and replace every placeholder. AUTH_SECRET can be generated with npx auth secret; TOKEN_ENCRYPTION_KEY must be an unpadded base64url encoding of 32 random bytes.

  2. Use an already-installed PostgreSQL 17 server or start the local Compose service:

    docker compose up -d postgres
    docker compose ps

    That service publishes PostgreSQL on loopback only, and its password is a committed, well-known string; POSTGRES_HOST_BIND widens that binding, so any address other than a loopback one publishes a database with known credentials to everything that can route to this machine. To reach it from another host, forward the loopback port over SSH — ssh -L 5432:127.0.0.1:5432 <host> — instead of widening the bind address.

  3. Point DATABASE_URL at that database, then install and migrate:

    pnpm install --frozen-lockfile
    pnpm db:migrate
  4. Start the application:

    pnpm dev

Useful verification commands (the geometry check needs a Chrome/Chromium binary and, when it spawns its own server, DATABASE_URL):

pnpm test --run
pnpm lint
pnpm typecheck
pnpm build
node scripts/check-page-geometry.mjs

CONTRIBUTING.md covers the rest of the development surface, including the conventions that reject work silently.

Operating an instance: GitHub OAuth and webhooks

This section is operator configuration for a deployment you run yourself; on the running instance it is already done. https://<public-host> is a placeholder for your own deployment's origin — replace it with that origin, and do not read it as an address to visit.

Create a GitHub OAuth application and a public HTTPS webhook endpoint. Configure the OAuth app's callback URL as https://<public-host>/api/auth/callback/github, and set these values in .env:

APP_URL=https://<public-host>
GITHUB_WEBHOOK_URL=https://<public-host>/api/github/webhooks
GITHUB_WEBHOOK_SECRET=<the-webhook-secret-configured-in-github>

GitHub must be able to reach the webhook URL over public HTTPS. Keep the webhook secret private and set the same value in GitHub and GITHUB_WEBHOOK_SECRET. The webhook endpoint rejects deliveries larger than 25 MiB with HTTP 413.

Operating an instance: the production service

The production deployment runs the application as a dedicated unprivileged system account rather than as root, under a systemd unit that keeps the filesystem read-only apart from the one cache directory Next writes at runtime. deploy/overflow.service is that unit, and deploy/README.md is the procedure that stands it up on a host, deploys a new revision under it, and rolls it back. tests/deploy/unit-file.test.ts fails if the unit loses any of that hardening.

Reconciliation

A repository is folded from a durable queue rather than inside the request that noticed it had fallen behind. A GitHub webhook delivery records a reconciliation job for the repository and answers immediately; when admission is available, an in-process worker normally claims that job within seconds, folds the repository, and clears the job. After claiming, a GraphQL budget hold can defer that repository to its sponsor's reset time, releasing the lease so the worker can continue to other repositories. A fold that throws is retried on the job after a minute, five, fifteen and an hour, and a repository that exhausts those retries stays visibly failed rather than disappearing from the queue. Registering a repository records the same kind of job, because the work already in the repository predates the webhook.

A sweep runs at startup and every six hours, and offers every active repository to that queue. A repository owns one job row, so a repository already queued keeps its place and its backoff, and one whose retries were exhausted is revived. Missed webhook deliveries and repositories left failed by a GitHub outage are therefore offered for repair within six hours without manual intervention; completion can take longer because of budget holds, queued work, or further GitHub failures.

GITHUB_GRAPHQL_BUDGET_RESERVE controls reconciliation's admission threshold. It defaults to 500 points; 0 disables the budget hold. Set a nonnegative integer: missing, blank or malformed values fall back to 500. A very large valid value deliberately makes the threshold restrictive, potentially holding every pass with a known current reading. Under the repository lock, after resolving its sponsor and before starting a run, the fold holds when that sponsor's recorded remaining balance is below this threshold. A held job is deferred to that reading's reset time without consuming retries; its outcome is BUDGET_HELD, distinct from a cooldown's DEFERRED. This admission check also applies to direct folds. An absent or expired reading, or an unavailable observer, permits admission until a usable reading is available.

This is an admission threshold, not a guaranteed remaining balance. An admitted pass may fetch many pull requests and paginate without a point ceiling, so it can spend past the reserve. The threshold stops new passes; it does not cancel requests within an admitted pass.

The GraphQL budget panel at the end of the moderator page (/moderation) displays one entry per known quota owner: the account's GitHub login when already available from the moderator roster, otherwise its account ID, followed by the recorded remaining balance, optional limit, reserve, reset time, observation time and hold state. Each sponsor account has its own OAuth quota; its readings and transitions are keyed by account ID, never by a credential. Unowned gateways record nothing. Within each owner's newest reset window the store retains the lowest balance and rejects older windows, so delayed responses cannot erase a known hold. Observations are process-local and disappear on restart; they are not shared across application instances. No owners observed yet is displayed separately from a known owner's unobserved or held budget.

For the production systemd deployment, edit GITHUB_GRAPHQL_BUDGET_RESERVE in /etc/overflow/overflow.env, then run sudo systemctl restart overflow.service to apply the new environment. In local development, update .env and restart pnpm dev. Restarting also clears the process-local observation, so the panel initially reports an unobserved budget.

Setting OVERFLOW_DISABLE_RECONCILIATION_SWEEP to any non-empty value turns off the worker as well as the sweep, which is the whole of automatic reconciliation: jobs still accumulate, and nothing drains them.

Run reconciliation explicitly when GitHub history must be re-read:

# Reconcile one explicit registered repository by owner/name.
pnpm reconcile --repository <owner>/<name>

# Reconcile every active registered repository.
pnpm reconcile

Reconciliation materializes issues, linked pull requests, settlement proof, self-work calibration, and unclaimed contributor records. PostgreSQL serializes each repository from snapshot collection through materialization. Eligibility is reconstructed at merge time from immutable moderation history, so a later sanction cannot rewrite eligible historical facts.

Upgrade existing webhook subscriptions

New registrations subscribe to issues, pull_request, pull_request_review, and issue_comment. Comment creation, editing and deletion each invalidate the payload's issue subject and queue repository reconciliation through the same path as issue events, regardless of the comment text, author or issue state. The fold's pricing, author/edit evidence rules and fifteen-minute grace are unchanged.

Deploy and verify the comment-capable release before upgrading existing hooks. With the deployment's DATABASE_URL, TOKEN_ENCRYPTION_KEY, and original GITHUB_WEBHOOK_SECRET loaded, run:

pnpm webhooks:upgrade

This enumerates active registrations, decrypts each sponsor's OAuth token, and resolves the current public repository by its immutable GitHub ID. It updates the persisted hook ID at that repository's current owner/name. GitHub's additive webhook update retains unrelated subscriptions; the command retains the hook's callback configuration and active state, and resends the original secret because GitHub warns that omitting it can remove it. Do not substitute a new secret or run this while someone is editing hook configuration. Hooks already covering all required events (including wildcard hooks) need no write.

Each JSON outcome identifies the registration by its local ID, reports subscription separately from queue, and names a sanitized failure stage. VERIFIED means the subscription was confirmed; QUEUED means a full upstream refresh was durably requested through the existing rederivation mechanism, not that the fold has finished. Historical missed comments have no known subject to invalidate, so this administrative repair bypasses incremental checkpoints. A hook that was disabled stays disabled. Every verified run requests full repair, including reruns after a queue failure. The summary counts succeeded and failed registrations. Exit 0 requires every registration to succeed; exit 1 indicates a failed step; exit 2 indicates invalid arguments. Missing tokens, missing/inaccessible hooks, lost admin rights, private repositories and mismatched IDs remain failures. Restore the sponsor's access or correct the reported registration problem, then rerun. UPGRADE_FAILED indicates configuration or enumeration failed before a complete summary was available.

Retain the JSON output and real exit status with the deployment record, as the ordinary deployment procedure does. The startup sweep reconciles evidence but does not upgrade subscriptions.

Continuous integration

GitHub Actions runs the complete gate on pushes to main, pull requests targeting main, and manual dispatches. The gate uses the pinned Node and pnpm versions, applies migrations to PostgreSQL 17, then runs pnpm test --run, pnpm lint, pnpm typecheck, and pnpm build, finishing with the page-geometry check, node scripts/check-page-geometry.mjs, against the built output. A separate actionlint/zizmor workflow validates and security-checks the workflow definitions themselves. All actions are commit-pinned and checkout credentials are not persisted.

Environment reference

.env.example documents every required setting:

Variable Purpose
DATABASE_URL PostgreSQL connection string
AUTH_SECRET Auth.js session signing secret
AUTH_GITHUB_ID, AUTH_GITHUB_SECRET GitHub OAuth application credentials
TOKEN_ENCRYPTION_KEY OAuth-token encryption key
APP_URL Public application URL; its origin is the only one browser mutations may come from, and a missing or malformed value refuses every one of them
GITHUB_WEBHOOK_URL, GITHUB_WEBHOOK_SECRET Public GitHub webhook URL and shared secret
MODERATOR_GITHUB_USER_IDS Comma-separated moderator GitHub account ids (gh api users/<login> --jq .id); replaces MODERATOR_GITHUB_LOGINS, which is no longer read
GITHUB_GRAPHQL_BUDGET_RESERVE Optional GraphQL admission threshold for new worker passes; defaults to 500, malformed values fall back to 500, and 0 disables the hold. A very large value is deliberately restrictive; see Reconciliation for scope and restart instructions.

Use placeholders only in checked-in configuration. Never commit OAuth credentials, webhook secrets, database passwords, or encryption keys.

License

MIT — see LICENSE. Contributions are accepted under the same terms.

About

Mutual-credit coordination for contributing spare LLM subscription capacity through GitHub issues and pull requests.

Topics

Resources

Code of conduct

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages