notgit/notgit-routes.ts

1693 lines
110 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* NotGit.org — Route handler
* "GitHub is a discovery layer, not a git host. Here's the migration."
*
* NotGit.org is a WellSpr.ing civic project. Published April 18, 2026.
* The formula is given freely under the WellSpr.ing covenant: freely given, so freely given.
*
* Same stack pattern as NoFlare.org — standalone SSR HTML pages, no SPA.
* Hosted on the same Replit deployment, domain-keyed via hostMatches.
*/
import type { Express, Request, Response } from "express";
import { db } from "./db";
import { sql } from "drizzle-orm";
const ADMIN_KEY = process.env.ADMIN_KEY || "b0db7a87384fc814b0f46ea7bdc6ab6a81152be5b098718b";
async function ensureNotgitTables() {
await db.execute(sql`
CREATE TABLE IF NOT EXISTS notgit_incident_reports (
id SERIAL PRIMARY KEY,
github_username TEXT,
suspension_date TEXT,
ticket_id TEXT,
work_description TEXT NOT NULL,
suspected_trigger TEXT,
resolution_status TEXT DEFAULT 'pending',
contact_email TEXT,
ip TEXT,
status TEXT NOT NULL DEFAULT 'received',
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
console.log("[NotGit] Incident report table ready");
}
// ── Favicon — git branch diverging into sovereignty ──────────────────────────
const NG_FAVICON = `data:image/svg+xml,%3Csvg viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3' fill='%23ff8c00'/%3E%3Ccircle cx='8' cy='24' r='3' fill='%23ff8c00' opacity='0.4'/%3E%3Ccircle cx='24' cy='12' r='3' fill='%23ff8c00'/%3E%3Cpath d='M8 11v10' stroke='%23ff8c00' stroke-width='1.5' opacity='0.4'/%3E%3Cpath d='M8 11 C8 16 24 15 24 15' stroke='%23ff8c00' stroke-width='1.5'/%3E%3Cpath d='M24 15 L28 10 M24 15 L28 20' stroke='%23ff8c00' stroke-width='1.2' stroke-linecap='round'/%3E%3C%2Fsvg%3E`;
// ── Shared CSS ────────────────────────────────────────────────────────────────
const NG_CSS = `
:root {
--bg: #080b10; --bg2: #0d1018; --bg3: #131820; --surface: #181f28;
--border: rgba(255,255,255,0.07); --border-accent: rgba(255,140,0,0.25);
--text: #dde4ee; --text2: #8898aa; --text3: #4a5568;
--amber: #ff8c00; --amber-dim: rgba(255,140,0,0.12); --amber-glow: rgba(255,140,0,0.05);
--green: #22c55e; --blue: #38bdf8;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'DM Sans', system-ui, sans-serif; background: var(--bg);
color: var(--text); line-height: 1.7; overflow-x: hidden;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
}
a { color: var(--amber); text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre { font-family: 'DM Mono', 'Fira Code', monospace; }
.container { max-width: 1100px; margin: 0 auto; padding: 0 2rem; }
.container-narrow { max-width: 760px; margin: 0 auto; padding: 0 2rem; }
/* ── Nav ── */
nav { position: sticky; top: 0; z-index: 100; background: rgba(8,11,16,0.92); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); }
.nav-inner { display: flex; align-items: center; justify-content: space-between; padding: 0 2rem; height: 56px; max-width: 1100px; margin: 0 auto; }
.nav-logo { font-family: 'Bebas Neue', sans-serif; font-size: 1.3rem; letter-spacing: 0.08em; color: var(--text); }
.nav-logo span { color: var(--amber); }
.nav-links { display: flex; gap: 2rem; }
.nav-links a { font-size: 0.82rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text2); text-decoration: none; transition: color 0.2s; }
.nav-links a:hover { color: var(--amber); text-decoration: none; }
/* ── Hero ── */
.hero { padding: 7rem 0 6rem; position: relative; overflow: hidden; }
.hero::before { content: ''; position: absolute; top: -200px; left: 50%; transform: translateX(-50%); width: 800px; height: 600px; background: radial-gradient(ellipse, var(--amber-glow) 0%, transparent 70%); pointer-events: none; }
.hero-eyebrow { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.75rem; }
.hero-eyebrow::before { content: ''; display: inline-block; width: 24px; height: 1px; background: var(--amber); }
h1.hero-title { font-family: 'Bebas Neue', sans-serif; font-size: clamp(3rem, 8vw, 6.5rem); line-height: 0.95; letter-spacing: 0.02em; margin-bottom: 2rem; }
h1.hero-title .dim { color: var(--text3); }
h1.hero-title .exit { color: var(--amber); }
.hero-sub { font-size: 1.1rem; color: var(--text2); max-width: 620px; margin-bottom: 3rem; line-height: 1.75; }
.hero-ctas { display: flex; gap: 1rem; flex-wrap: wrap; }
.btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.85rem 1.75rem; font-size: 0.9rem; font-weight: 500; letter-spacing: 0.03em; cursor: pointer; transition: all 0.2s; border: none; font-family: 'DM Sans', sans-serif; }
.btn-primary { background: var(--amber); color: #080b10; }
.btn-primary:hover { background: #ffaa33; text-decoration: none; color: #080b10; }
.btn-secondary { background: transparent; color: var(--text2); border: 1px solid var(--border); }
.btn-secondary:hover { border-color: var(--amber); color: var(--amber); text-decoration: none; }
/* ── Sections ── */
section { padding: 6rem 0; border-top: 1px solid var(--border); }
section.alt { background: var(--bg2); }
.section-label { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); margin-bottom: 1rem; }
h2.section-title { font-family: 'Bebas Neue', sans-serif; font-size: clamp(2rem, 5vw, 3.5rem); letter-spacing: 0.03em; line-height: 1.1; margin-bottom: 1.5rem; }
.section-body { color: var(--text2); font-size: 1rem; line-height: 1.8; }
.section-body p { margin-bottom: 1.25rem; }
/* ── Timeline ── */
.timeline-box { background: var(--surface); border: 1px solid var(--border-accent); padding: 1.5rem; margin: 2.5rem 0; overflow-x: auto; }
.timeline-box pre { font-family: 'DM Mono', monospace; font-size: 0.82rem; color: var(--text2); line-height: 1.9; white-space: pre; }
.timeline-box pre .ts { color: var(--amber); }
.timeline-box pre .evt { color: var(--text); }
/* ── Callout ── */
.callout { background: var(--amber-dim); border-left: 3px solid var(--amber); padding: 1.5rem 2rem; margin: 2.5rem 0; font-family: 'DM Serif Display', serif; font-size: 1.1rem; font-style: italic; line-height: 1.65; color: var(--text); }
/* ── Evidence links ── */
.evidence-links { margin-top: 2.5rem; }
.evidence-link { display: flex; align-items: flex-start; gap: 1rem; padding: 1rem 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.evidence-link:first-child { border-top: 1px solid var(--border); }
.ev-tag { font-family: 'DM Mono', monospace; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.2rem 0.6rem; border: 1px solid var(--border-accent); color: var(--amber); flex-shrink: 0; margin-top: 0.15rem; }
.ev-text a { color: var(--text); font-weight: 500; }
.ev-text a:hover { color: var(--amber); }
.ev-desc { font-size: 0.82rem; color: var(--text3); margin-top: 0.2rem; }
/* ── Pattern section ── */
.case-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 2.5rem; }
.case-card { background: var(--surface); border: 1px solid var(--border); padding: 1.5rem; }
.case-project { font-family: 'DM Mono', monospace; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--amber); margin-bottom: 0.5rem; }
.case-desc { font-size: 0.88rem; color: var(--text2); line-height: 1.6; }
.case-stat { font-family: 'DM Mono', monospace; font-size: 0.78rem; color: var(--text3); margin-top: 0.75rem; }
.pullquote { background: var(--bg3); border: 1px solid var(--border-accent); padding: 2rem; margin: 2.5rem 0; }
.pullquote blockquote { font-family: 'DM Serif Display', serif; font-size: 1.15rem; font-style: italic; color: var(--text); line-height: 1.65; }
.pullquote cite { display: block; margin-top: 1rem; font-family: 'DM Mono', monospace; font-size: 0.75rem; color: var(--text3); font-style: normal; }
/* ── Architecture diagram ── */
.arch-diagram { background: var(--surface); border: 1px solid var(--border-accent); padding: 2rem; margin: 2.5rem 0; overflow-x: auto; }
.arch-diagram pre { font-family: 'DM Mono', monospace; font-size: 0.8rem; color: var(--text2); line-height: 1.7; white-space: pre; }
.arch-diagram pre .hl { color: var(--amber); }
/* ── Three steps ── */
.steps { margin-top: 2.5rem; }
.step { display: flex; gap: 2rem; padding: 2rem 0; border-bottom: 1px solid var(--border); }
.step:first-child { border-top: 1px solid var(--border); }
.step-num { font-family: 'Bebas Neue', sans-serif; font-size: 3rem; color: var(--amber); line-height: 1; flex-shrink: 0; width: 52px; }
.step-title { font-size: 1.05rem; font-weight: 600; color: var(--text); margin-bottom: 0.5rem; }
.step-body { font-size: 0.92rem; color: var(--text2); line-height: 1.75; }
.step-body code { background: var(--surface); padding: 0.15rem 0.4rem; font-size: 0.83rem; color: var(--amber); border: 1px solid var(--border); }
/* ── Comparison table ── */
.compare-wrap { overflow-x: auto; margin-top: 2.5rem; }
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
thead { background: var(--surface); }
th { padding: 1rem 1.25rem; text-align: left; font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text3); font-weight: 400; border-bottom: 1px solid var(--border); }
td { padding: 0.9rem 1.25rem; border-bottom: 1px solid var(--border); color: var(--text2); vertical-align: top; }
tr:hover td { background: var(--surface); }
td strong { color: var(--text); }
td .highlight { color: var(--amber); font-weight: 500; }
.best-col { background: var(--amber-dim); }
/* ── AI rules ── */
.rules-list { margin-top: 2rem; }
.rule { display: flex; gap: 1.5rem; padding: 1.5rem 0; border-bottom: 1px solid var(--border); }
.rule:first-child { border-top: 1px solid var(--border); }
.rule-num { font-family: 'DM Mono', monospace; font-size: 0.78rem; color: var(--amber); flex-shrink: 0; width: 28px; padding-top: 0.1rem; }
.rule-title { font-weight: 600; color: var(--text); margin-bottom: 0.4rem; font-size: 0.95rem; }
.rule-body { font-size: 0.88rem; color: var(--text2); line-height: 1.7; }
/* ── Form ── */
.form-wrap { background: var(--surface); border: 1px solid var(--border); padding: 2.5rem; max-width: 680px; margin-top: 2.5rem; }
.form-row { margin-bottom: 1.5rem; }
label { display: block; font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text3); margin-bottom: 0.5rem; }
input[type=text], input[type=email], textarea, select {
width: 100%; background: var(--bg); border: 1px solid var(--border); color: var(--text);
font-family: 'DM Sans', sans-serif; font-size: 0.93rem; padding: 0.75rem 1rem;
outline: none; transition: border-color 0.2s; resize: vertical;
}
input:focus, textarea:focus, select:focus { border-color: var(--amber); }
input::placeholder, textarea::placeholder { color: var(--text3); }
select option { background: var(--bg2); }
textarea { min-height: 100px; }
.form-note { font-size: 0.8rem; color: var(--text3); margin-top: 0.4rem; }
.form-submit { margin-top: 2rem; }
.form-success { display: none; background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.3); padding: 1.5rem; color: #6ee7a0; font-size: 0.92rem; }
/* ── Footer ── */
footer { background: var(--bg3); border-top: 1px solid var(--border); padding: 4rem 0 2.5rem; }
.footer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 3rem; margin-bottom: 3rem; }
.footer-col-title { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text3); margin-bottom: 1.25rem; }
.footer-col a { display: block; font-size: 0.88rem; color: var(--text2); margin-bottom: 0.6rem; }
.footer-col a:hover { color: var(--amber); text-decoration: none; }
.footer-bottom { border-top: 1px solid var(--border); padding-top: 2rem; font-size: 0.82rem; color: var(--text3); line-height: 1.7; }
.footer-bottom a { color: var(--text3); }
.footer-bottom a:hover { color: var(--amber); }
/* ── Standalone page (formula, push-mirror, incident) ── */
.standalone { padding: 4rem 0 6rem; }
.standalone-eyebrow { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); margin-bottom: 1rem; }
.standalone h1 { font-family: 'Bebas Neue', sans-serif; font-size: clamp(2rem, 5vw, 3.5rem); letter-spacing: 0.03em; line-height: 1.1; margin-bottom: 1rem; }
.standalone .sub { font-size: 1.05rem; color: var(--text2); max-width: 620px; margin-bottom: 3rem; line-height: 1.75; }
.standalone h2 { font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; letter-spacing: 0.03em; margin: 3rem 0 1rem; color: var(--text); }
.standalone h3 { font-size: 1.05rem; font-weight: 600; color: var(--text); margin: 2rem 0 0.75rem; }
.standalone p { color: var(--text2); font-size: 0.97rem; line-height: 1.8; margin-bottom: 1rem; }
.standalone ul, .standalone ol { color: var(--text2); font-size: 0.95rem; line-height: 1.8; padding-left: 1.5rem; margin-bottom: 1.25rem; }
.standalone li { margin-bottom: 0.4rem; }
.standalone code { background: var(--surface); padding: 0.15rem 0.4rem; font-size: 0.83rem; color: var(--amber); border: 1px solid var(--border); font-family: 'DM Mono', monospace; }
.codeblock { background: var(--surface); border: 1px solid var(--border); padding: 1.5rem; margin: 1.5rem 0; overflow-x: auto; }
.codeblock pre { font-family: 'DM Mono', monospace; font-size: 0.82rem; color: var(--text2); line-height: 1.8; white-space: pre; }
.codeblock .comment { color: var(--text3); }
.codeblock .cmd { color: var(--amber); }
.codeblock .str { color: var(--green); }
.tip { background: var(--amber-dim); border-left: 3px solid var(--amber); padding: 1rem 1.5rem; margin: 1.5rem 0; font-size: 0.9rem; color: var(--text2); }
.tip strong { color: var(--amber); }
.divider { border: none; border-top: 1px solid var(--border); margin: 3rem 0; }
@media (max-width: 720px) {
.nav-links { display: none; }
.hero-ctas { flex-direction: column; }
.step { flex-direction: column; gap: 1rem; }
.stack-row { grid-template-columns: 1fr; gap: 2rem; }
h1.hero-title { font-size: 3.2rem; }
}
`;
const NG_FONTS = `
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Serif+Display:ital@0;1&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
`;
const NG_NAV = `
<nav>
<div class="nav-inner">
<a href="/" class="nav-logo" style="text-decoration:none">Not<span>Git</span>.org</a>
<div class="nav-links">
<a href="#evidence">Evidence</a>
<a href="#pattern">The Pattern</a>
<a href="#exit">Exit Guide</a>
<a href="#ai-agents">AI Agents</a>
<a href="/report">Report</a>
<a href="/formula">Formula</a>
<a href="/replit" style="color:var(--amber)">Replit Guide</a>
</div>
</div>
</nav>
`;
const NG_FOOTER = `
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<div class="footer-col-title">Evidence & Cases</div>
<a href="/incident/2026-04-18">April 18, 2026 Incident (WellBuilder)</a>
<a href="https://www.reddit.com/r/github/comments/1jsimjp/github_keeps_suspending_accounts_without_notice/" target="_blank" rel="noopener">r/github Thread (external ↗)</a>
<a href="/report">Submit an Incident</a>
</div>
<div class="footer-col">
<div class="footer-col-title">Migration Tools</div>
<a href="/formula">The Three-Mirror Formula</a>
<a href="/push-mirror-setup">Push-Mirror Setup Walkthrough</a>
<a href="/replit">Replit User Guide</a>
<a href="#exit">Railway Forgejo Deploy Guide</a>
<a href="#ai-agents">AI-Agent-Era Operational Rules</a>
</div>
<div class="footer-col">
<div class="footer-col-title">Context</div>
<a href="https://noflare.org">NoFlare.org (sister project)</a>
<a href="https://wellspr.ing">WellSpr.ing (publishing institution)</a>
<a href="https://wellspr.ing/blog/sovereignty-the-github-response">The Sovereignty Blog Post</a>
<a href="https://wellspr.ing/constitution">Eight Principles</a>
<a href="https://wellspr.ing/mcp/federation.json">Federation Manifest (v2.0)</a>
</div>
</div>
<div class="footer-bottom">
NotGit.org is a WellSpr.ing civic project. Published April 18, 2026 — the day of the WellBuilder suspension.<br>
The formula is given freely under the WellSpr.ing covenant: <em>freely given, so freely given.</em><br>
No subscription, no sign-up, no tracking beyond the incident-submission form's opt-in disclosure.<br>
<a href="https://wellspr.ing/sovereignty">Architectural Sovereignty Notice</a> · <a href="https://noflare.org">NoFlare.org</a> · <a href="mailto:ody@wellspr.ing">ody@wellspr.ing</a>
</div>
</div>
</footer>
`;
function ngHead(title: string, desc: string, path = "") {
const url = `https://notgit.org${path}`;
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<meta name="description" content="${desc}">
<meta property="og:title" content="${title}">
<meta property="og:description" content="${desc}">
<meta property="og:type" content="website">
<meta property="og:url" content="${url}">
<meta property="og:site_name" content="NotGit.org">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${title}">
<meta name="twitter:description" content="${desc}">
<link rel="canonical" href="${url}">
<link rel="icon" type="image/svg+xml" href="${NG_FAVICON}">
${NG_FONTS}
<style>${NG_CSS}</style>
</head>
<body>`;
}
// ── Home page ─────────────────────────────────────────────────────────────────
const NOTGIT_HOME = ngHead(
"NotGit.org — GitHub is a discovery layer, not a git host.",
"When GitHub suspends your account without notice, you don't need GitHub. The migration from GitHub-dependent to git-sovereign takes 15 minutes. Formula, tooling, incident record.",
) + NG_NAV + `
<!-- ── Hero ── -->
<main>
<div class="hero">
<div class="container">
<div class="hero-eyebrow">Published April 18, 2026 · WellSpr.ing Civic Project</div>
<h1 class="hero-title">
GitHub is a<br>
<span class="dim">discovery layer.</span><br>
<span class="exit">Not a git host.</span><br>
Here's the migration.
</h1>
<p class="hero-sub">
When GitHub suspends your account without notice, without graduated enforcement,
and without substantive staff response for 3090 days, the fastest answer
is not to plead with the support queue. The fastest answer is to own your git,
mirror to GitHub as discovery surface, and keep working.
</p>
<div class="hero-ctas">
<a href="#exit" class="btn btn-primary">Get Your Exit Plan →</a>
<a href="#evidence" class="btn btn-secondary">Read the Incident Record</a>
</div>
</div>
</div>
<!-- ── Evidence ── -->
<section id="evidence">
<div class="container">
<div class="section-label">The Incident</div>
<h2 class="section-title">What happened to WellBuilder<br>on April 18, 2026</h2>
<div class="section-body">
<p>On the morning of April 18, 2026, at approximately 8:30 AM PT, GitHub suspended the WellBuilder organization. WellBuilder publishes civic-technology MCP servers — one per US area code — as part of the WellSpr.ing covenant-governed civic infrastructure federation. Thirty-two repositories were offline simultaneously. The personal account that owned the organization was also suspended.</p>
<p>No suspension email. No notice. No graduated enforcement. The appeal path, when opened, routed first-line response to an automated reply requesting information already present in the appeal letter. WellSpr.ing responded within minutes with complete information. The account remains suspended at the time of this site's publication.</p>
</div>
<div class="timeline-box">
<pre><span class="ts">08:30 AM PT</span> <span class="evt">— WellBuilder organization suspended (all 32 repos offline)</span>
<span class="ts">08:48 AM PT</span> <span class="evt">— WellSpr.ing notified; unable to access account</span>
<span class="ts">08:48 AM PT</span> <span class="evt">— Appeal submitted via support.github.com</span>
<span class="ts">~08:54 AM PT</span> <span class="evt">— Auto-reply requests standard identification details</span>
<span class="ts">08:55 AM PT</span> <span class="evt">— WellSpr.ing replies with complete information</span>
<span class="ts">— </span> <span class="evt">— Substantive response pending as of publication</span></pre>
</div>
<div class="callout">
Thirty-two MCP servers. One per US area code. Federated under the Linux Foundation's own standardized protocol. Each repository complete, each serving real civic data, each under a published conduct standard. Suspended simultaneously. No email. No reason given.
</div>
<div class="evidence-links">
<div class="evidence-link">
<span class="ev-tag">Stable URL</span>
<div class="ev-text">
<a href="/incident/2026-04-18">notgit.org/incident/2026-04-18</a>
<div class="ev-desc">Permanent incident record for the WellBuilder case</div>
</div>
</div>
<div class="evidence-link">
<span class="ev-tag">Blog Post</span>
<div class="ev-text">
<a href="https://wellspr.ing/blog/sovereignty-the-github-response" target="_blank">WellSpr.ing sovereignty response</a>
<div class="ev-desc">Full account of the incident and the architectural response</div>
</div>
</div>
<div class="evidence-link">
<span class="ev-tag">Manifest</span>
<div class="ev-text">
<a href="https://wellspr.ing/mcp/federation.json" target="_blank">Federation manifest v2.0 (sovereign architecture)</a>
<div class="ev-desc">Updated to list primary Forgejo host and Codeberg mirror for all 32 portals</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── Pattern ── -->
<section id="pattern" class="alt">
<div class="container">
<div class="section-label">The Pattern</div>
<h2 class="section-title">Documented across sixty cases</h2>
<div class="section-body">
<p>The WellBuilder case is not anomalous. A Reddit thread on r/github, maintained by volunteers since 2023, has catalogued approximately sixty suspension cases with the same signature: no email, no graduated enforcement, appeals that queue for 30 to 90 days, and — documented by multiple users — manual reinstatement by staff followed by automated re-suspension within 1 to 5 days.</p>
<p>The affected maintainers are not spammers.</p>
</div>
<div class="case-grid">
<div class="case-card">
<div class="case-project">Limine Bootloader</div>
<div class="case-desc">Main maintainer of Limine, a widely-adopted open-source boot manager used across hundreds of projects. Suspended without notice.</div>
<div class="case-stat">Status: public record, r/github thread</div>
</div>
<div class="case-card">
<div class="case-project">GetX Framework</div>
<div class="case-desc">Creator of GetX, the Flutter state management framework installed in 200,000+ projects. Suspended without notice or graduated enforcement.</div>
<div class="case-stat">200,000+ downstream projects affected</div>
</div>
<div class="case-card">
<div class="case-project">AI Research · 62 repos</div>
<div class="case-desc">Independent AI researcher with paid GitHub Pro account, 62 research repositories, suspended over a Git LFS model checkpoint. Had to appeal from a second paid account.</div>
<div class="case-stat">Paid Pro subscriber · No email sent</div>
</div>
<div class="case-card">
<div class="case-project">Ferdium · Core Maintainer</div>
<div class="case-desc">One of two core maintainers of Ferdium, an open-source messaging app. Eight-year-old GitHub account. Suspended without notice.</div>
<div class="case-stat">8-year account history · No warning</div>
</div>
<div class="case-card">
<div class="case-project">Cybersecurity Research</div>
<div class="case-desc">Security researcher publishing indicators-of-compromise to help others defend against malware. Suspended for publishing defensive security data.</div>
<div class="case-stat">Defensive research — flagged as threat</div>
</div>
<div class="case-card">
<div class="case-project">WellBuilder · 32 Repos</div>
<div class="case-desc">Civic infrastructure federation — 32 MCP servers, one per US area code, serving local civic data through the Linux Foundation's standardized protocol.</div>
<div class="case-stat">April 18, 2026 · This incident</div>
</div>
</div>
<div class="pullquote">
<blockquote>"GitHub suspended my paid account with 62 repos, sent no email and I had to file appeals from a second paid account... If GitHub can do this to a paying customer over a model checkpoint, they can do it to you over anything."</blockquote>
<cite>— Independent AI researcher, April 2026, r/github thread</cite>
</div>
<div class="section-body">
<p>GitHub's own Terms of Service Section F requires notice before suspension. The enforcement documentation describes graduated enforcement before termination. Across sixty documented cases, neither commitment appears to be consistently honored.</p>
<p style="color:var(--text);"><strong>This site is not about whether GitHub is a net-positive for open-source software.</strong> It clearly is, and has been, for more than a decade. This site is about the specific gap between GitHub's stated enforcement commitments and actual enforcement practice — and about the fact that the correction to that gap is individually available to every maintainer. You do not need to wait for GitHub to fix this. You can migrate in an afternoon.</p>
</div>
</div>
</section>
<!-- ── Exit Guide ── -->
<section id="exit">
<div class="container">
<div class="section-label">The Exit Guide</div>
<h2 class="section-title">The three-mirror architecture</h2>
<div class="section-body">
<p>The core insight: <strong style="color:var(--text)">GitHub is a discovery layer, not a git host.</strong> The conflation of those two functions is the dependency trap. The formula separates them back out.</p>
</div>
<div class="arch-diagram" style="background:transparent;border:none;padding:0;overflow:visible">
<svg viewBox="0 0 700 320" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:700px;display:block;margin:0 auto 1rem" role="img" aria-label="Three-mirror architecture diagram">
<defs>
<marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
<path d="M0,0 L0,6 L8,3 z" fill="#ff8c00" opacity="0.7"/>
</marker>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- PRIMARY box -->
<rect x="150" y="10" width="400" height="90" rx="6" fill="#131820" stroke="#ff8c00" stroke-width="1.5" filter="url(#glow)"/>
<rect x="150" y="10" width="400" height="3" rx="2" fill="#ff8c00" opacity="0.6"/>
<text x="350" y="38" text-anchor="middle" fill="#ff8c00" font-family="DM Mono,monospace" font-size="11" letter-spacing="2" font-weight="700">PRIMARY GIT — SOVEREIGN</text>
<text x="350" y="60" text-anchor="middle" fill="#dde4ee" font-family="DM Sans,system-ui,sans-serif" font-size="13.5">git.yourproject.org</text>
<text x="350" y="79" text-anchor="middle" fill="#8898aa" font-family="DM Sans,system-ui,sans-serif" font-size="11.5">Self-hosted Forgejo · Railway / Fly.io · You own it</text>
<!-- left arrow -->
<line x1="270" y1="100" x2="163" y2="202" stroke="#ff8c00" stroke-width="1.5" stroke-dasharray="5,3" opacity="0.6" marker-end="url(#arr)"/>
<text x="175" y="158" fill="#8898aa" font-family="DM Mono,monospace" font-size="10" letter-spacing="1" transform="rotate(-28,175,158)">auto push-mirror</text>
<!-- right arrow -->
<line x1="430" y1="100" x2="537" y2="202" stroke="#ff8c00" stroke-width="1.5" stroke-dasharray="5,3" opacity="0.6" marker-end="url(#arr)"/>
<text x="478" y="155" fill="#8898aa" font-family="DM Mono,monospace" font-size="10" letter-spacing="1" transform="rotate(28,478,155)">auto push-mirror</text>
<!-- SECONDARY box -->
<rect x="10" y="210" width="290" height="100" rx="6" fill="#131820" stroke="rgba(255,255,255,0.12)" stroke-width="1.2"/>
<text x="155" y="235" text-anchor="middle" fill="#94a3b8" font-family="DM Mono,monospace" font-size="10" letter-spacing="2">SECONDARY — COMMUNITY</text>
<text x="155" y="257" text-anchor="middle" fill="#dde4ee" font-family="DM Sans,system-ui,sans-serif" font-size="13.5">codeberg.org/yourproject</text>
<text x="155" y="276" text-anchor="middle" fill="#8898aa" font-family="DM Sans,system-ui,sans-serif" font-size="11">Nonprofit · EU-hosted · Forgejo-native</text>
<text x="155" y="295" text-anchor="middle" fill="#22c55e" font-family="DM Mono,monospace" font-size="10" letter-spacing="1">Free · Your fallback</text>
<!-- DISCOVERY box -->
<rect x="400" y="210" width="290" height="100" rx="6" fill="#131820" stroke="rgba(255,255,255,0.07)" stroke-width="1.2"/>
<text x="545" y="235" text-anchor="middle" fill="#94a3b8" font-family="DM Mono,monospace" font-size="10" letter-spacing="2">DISCOVERY LAYER</text>
<text x="545" y="257" text-anchor="middle" fill="#dde4ee" font-family="DM Sans,system-ui,sans-serif" font-size="13.5">github.com/yourproject</text>
<text x="545" y="276" text-anchor="middle" fill="#8898aa" font-family="DM Sans,system-ui,sans-serif" font-size="11">Stars · Forks · Network effect</text>
<text x="545" y="295" text-anchor="middle" fill="#f87171" font-family="DM Mono,monospace" font-size="10" letter-spacing="1">Suspendable — non-critical</text>
</svg>
</div>
<div class="steps">
<div class="step">
<div class="step-num">01</div>
<div>
<div class="step-title">Primary git — self-hosted Forgejo</div>
<div class="step-body">
Deploy on Railway, Fly.io, or any VPS. Forgejo is the community fork of Gitea, actively maintained, open source, AGPL-licensed. Deploy time on Railway: ~15 minutes. Cost: $510/month. CNAME <code>git.yourproject.org</code> to your Railway deployment. This is where you push. If GitHub or Codeberg or any other host disappears tomorrow, your git source of truth is unaffected.
</div>
</div>
</div>
<div class="step">
<div class="step-num">02</div>
<div>
<div class="step-title">Secondary git — Codeberg</div>
<div class="step-body">
Codeberg is the community-governed, nonprofit Forgejo instance hosted in Berlin. Free for open source. Runs Forgejo natively, so push-mirror integration with your primary is trivial to configure. This is your fallback — what if your self-hosted instance has a bad day. Configure a push-mirror from your Forgejo primary: one API call, automatic thereafter. Full walkthrough at <a href="/push-mirror-setup">notgit.org/push-mirror-setup</a>.
</div>
</div>
</div>
<div class="step">
<div class="step-num">03</div>
<div>
<div class="step-title">Discovery layer — GitHub (as mirror)</div>
<div class="step-body">
GitHub, if and when your account is restored, becomes a push-mirror only. You never push to it directly. Your primary Forgejo pushes to it automatically. If GitHub suspends you again, your stars and forks are frozen but your git work continues unaffected. Suspension becomes a discoverability event, not a data-loss event. GitHub, at its best, holds a first-class place in your federation. That place just isn't primary anymore.
</div>
</div>
</div>
</div>
<div class="callout">
What makes this resilient: Forgejo's native push-mirror feature means you configure the two secondary mirrors once. Your workflow doesn't change — <code>git push origin main</code> to your sovereign primary, and it syncs automatically. Suspension of any single mirror is a discoverability event, not a data-loss event.
</div>
<p style="margin-top:2rem"><a href="/formula" class="btn btn-primary">Full Migration Formula →</a>&nbsp;&nbsp;<a href="/push-mirror-setup" class="btn btn-secondary">Push-Mirror Technical Walkthrough</a></p>
</div>
</section>
<!-- ── Platform comparison ── -->
<section class="alt">
<div class="container">
<div class="section-label">Choose Your Mirrors</div>
<h2 class="section-title">Platform comparison</h2>
<div class="compare-wrap">
<table>
<thead>
<tr>
<th></th>
<th class="best-col">Forgejo (self-hosted)</th>
<th>Codeberg</th>
<th>SourceHut</th>
<th>GitLab CE</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Governance</strong></td>
<td class="best-col"><span class="highlight">You own it</span></td>
<td>Nonprofit</td>
<td>Nonprofit</td>
<td>You own it</td>
</tr>
<tr>
<td><strong>Cost</strong></td>
<td class="best-col"><span class="highlight">~$510/mo</span></td>
<td>Free</td>
<td>Free / paid</td>
<td>Heavier infra</td>
</tr>
<tr>
<td><strong>PAT / API</strong></td>
<td class="best-col">Full Forgejo API</td>
<td>Full Forgejo API</td>
<td>REST + email workflow</td>
<td>Full GitLab API</td>
</tr>
<tr>
<td><strong>Push mirrors</strong></td>
<td class="best-col"><span class="highlight">Yes — native</span></td>
<td>Yes — native</td>
<td>Limited</td>
<td>Yes</td>
</tr>
<tr>
<td><strong>Speed to deploy</strong></td>
<td class="best-col"><span class="highlight">15 min (Railway)</span></td>
<td>Instant (signup)</td>
<td>Instant (signup)</td>
<td>Hours (self-host)</td>
</tr>
<tr>
<td><strong>Best for</strong></td>
<td class="best-col"><span class="highlight">Primary sovereign</span></td>
<td>Community secondary</td>
<td>Minimalist / CLI-first</td>
<td>Org-scale</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ── AI Agents ── -->
<section id="ai-agents">
<div class="container">
<div class="section-label">AI Agents & Mirrors</div>
<h2 class="section-title">Using AI coding tools<br>without triggering enforcement</h2>
<div class="section-body">
<p>A distinct class of suspension cases involves maintainers using AI coding assistants — Claude, Cursor, Copilot Workspace, custom MCP servers — with personal access tokens that make API calls GitHub's detection flags as anomalous. Documented cases include maintainers whose Claude Desktop + GitHub MCP integration was flagged as "token abuse," researchers whose Git LFS pushes of ML checkpoints triggered suspension, and builders whose rapid automation of repository creation (for legitimate federated projects) matched typosquatting heuristics.</p>
<p>These cases are growing. The tooling is ahead of the platform's detection calibration. The mitigation is operational.</p>
</div>
<div class="rules-list">
<div class="rule">
<div class="rule-num">01</div>
<div>
<div class="rule-title">README-first publication</div>
<div class="rule-body">Never publish an empty or near-empty repository. Substantive README, working initial commit, topic tags, a license. Repos created via automation that look like placeholders will flag — this is what happened to WellBuilder.</div>
</div>
</div>
<div class="rule">
<div class="rule-num">02</div>
<div>
<div class="rule-title">Cadence discipline</div>
<div class="rule-body">Don't create twenty repositories in ten minutes. If automation is creating repos, rate-limit it to 23 per day. Rapid federated publication via PAT automation resembles typosquatting patterns to detection systems. Defensive discipline regardless of platform.</div>
</div>
</div>
<div class="rule">
<div class="rule-num">03</div>
<div>
<div class="rule-title">PAT scoping</div>
<div class="rule-body">Give your AI assistant's PAT the minimum scope it needs. A PAT with full-org permissions used by an AI that makes many API calls per minute is exactly the pattern that flags. Scope to <code>repo</code> only, not <code>admin:org</code> unless necessary.</div>
</div>
</div>
<div class="rule">
<div class="rule-num">04</div>
<div>
<div class="rule-title">Push-mirror through sovereign primary</div>
<div class="rule-body">If you're using AI tooling that pushes to git, push to your sovereign Forgejo first, not to GitHub. The push-mirror propagates to GitHub. If GitHub flags the pattern, your primary is untouched and the worst case is you turn off the GitHub mirror temporarily while you resolve it.</div>
</div>
</div>
<div class="rule">
<div class="rule-num">05</div>
<div>
<div class="rule-title">Pre-commit model checkpoints elsewhere</div>
<div class="rule-body">Git LFS for ML work has documented edge cases. If you're working with large model artifacts, consider a dedicated artifact store (Hugging Face, Weights & Biases) and reference from git rather than committing directly. Git is not an artifact store.</div>
</div>
</div>
</div>
<div class="callout" style="margin-top:2.5rem">
The underlying principle: AI coding tools will continue outpacing platform detection calibration. The operational discipline is to use those tools against sovereign primary hosts where no single platform's classifier can take your work offline. Your AI assistant gets the same workflow. The platform becomes a mirror.
</div>
</div>
</section>
<!-- ── Report CTA ── -->
<section class="alt">
<div class="container" style="text-align:center">
<div class="section-label">Submit Your Case</div>
<h2 class="section-title">The pattern gets legible<br>through documentation</h2>
<p style="color:var(--text2);max-width:580px;margin:0 auto 2.5rem;font-size:1rem;line-height:1.8">If your GitHub account has been suspended without notice, without graduated enforcement, or re-suspended after manual reinstatement, your case belongs in the corpus. Submissions inform periodic pattern reports and help other maintainers recognize what's happening in their own cases.</p>
<a href="/report" class="btn btn-primary">Submit an Incident Report →</a>
</div>
</section>
</main>
${NG_FOOTER}
<script>
document.querySelectorAll('form[data-notgit-report]').forEach(form => {
form.addEventListener('submit', async e => {
e.preventDefault();
const btn = form.querySelector('button[type=submit]');
btn.disabled = true; btn.textContent = 'Submitting...';
try {
const data = Object.fromEntries(new FormData(form));
const r = await fetch('/api/notgit/report', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
if (r.ok) {
form.style.display = 'none';
document.getElementById('notgit-success').style.display = 'block';
} else { btn.disabled = false; btn.textContent = 'Submit Report'; alert('Submission failed. Please try again.'); }
} catch { btn.disabled = false; btn.textContent = 'Submit Report'; alert('Network error. Please try again.'); }
});
});
</script>
</body></html>`;
// ── /formula — Standalone migration guide ─────────────────────────────────────
const FORMULA_HTML = ngHead(
"The Three-Mirror Formula — NotGit.org",
"The complete git sovereignty migration: self-hosted Forgejo primary, Codeberg secondary, GitHub as discovery mirror. Executable in an afternoon.",
"/formula",
) + NG_NAV + `
<main>
<div class="standalone">
<div class="container-narrow">
<div class="standalone-eyebrow">NotGit.org / Formula</div>
<h1>The Three-Mirror Formula</h1>
<p class="sub">The complete migration from GitHub-dependent to git-sovereign. Executable in an afternoon. Given freely — freely given, so freely given.</p>
<hr class="divider">
<h2>The Thesis</h2>
<p>Git is a protocol. GitHub is a discovery layer built on top of that protocol. The dependency trap is conflating the two. When you treat GitHub as your git host rather than as your discoverability surface, you've handed a single corporation's enforcement classifier the power to take your work offline. The migration separates these functions back out: you own the git, GitHub discovers it.</p>
<p>The migration takes 15 minutes of active configuration. The rest is your regular workflow, unchanged. You still run <code>git push origin main</code>. Your sovereign primary handles the rest.</p>
<h2>Step 1 — Deploy Forgejo on Railway</h2>
<p>Railway is the fastest path to a running Forgejo instance. Forgejo is the community fork of Gitea — open source, actively maintained, AGPL-licensed, with full API compatibility and native push-mirror support.</p>
<div class="codeblock"><pre><span class="comment"># 1. Go to railway.app and create a new project</span>
<span class="comment"># 2. Deploy from template: search "Forgejo" in the Railway template gallery</span>
<span class="comment"># 3. Add a PostgreSQL service to the project (for metadata persistence)</span>
<span class="comment"># 4. Set environment variables:</span>
<span class="cmd">FORGEJO__database__DB_TYPE</span>=postgres
<span class="cmd">FORGEJO__database__HOST</span>=<span class="str">\${{Postgres.PGHOST}}</span>
<span class="cmd">FORGEJO__database__NAME</span>=<span class="str">\${{Postgres.PGDATABASE}}</span>
<span class="cmd">FORGEJO__database__USER</span>=<span class="str">\${{Postgres.PGUSER}}</span>
<span class="cmd">FORGEJO__database__PASSWD</span>=<span class="str">\${{Postgres.PGPASSWORD}}</span>
<span class="cmd">FORGEJO__server__DOMAIN</span>=<span class="str">git.yourproject.org</span>
<span class="cmd">FORGEJO__server__ROOT_URL</span>=<span class="str">https://git.yourproject.org</span></pre>
</div>
<p>After deploy, visit the Railway-assigned URL, complete the install wizard, and create your admin account. Total time: ~15 minutes.</p>
<div class="tip"><strong>CNAME it:</strong> Add <code>git CNAME yourapp.up.railway.app</code> at your domain registrar. This gives you <code>git.yourproject.org</code> as the permanent address, decoupled from the Railway deployment URL.</div>
<h2>Step 2 — Create your organization and repositories</h2>
<p>Create an organization on your Forgejo instance that mirrors your GitHub org. Create your repositories. Push your existing code:</p>
<div class="codeblock"><pre><span class="comment"># Add your Forgejo instance as a remote</span>
<span class="cmd">git remote add sovereign https://git.yourproject.org/yourorg/yourrepo.git</span>
<span class="comment"># Push all branches and tags</span>
<span class="cmd">git push sovereign --all</span>
<span class="cmd">git push sovereign --tags</span>
<span class="comment"># Update your primary remote so daily workflow goes to sovereign</span>
<span class="cmd">git remote set-url origin https://git.yourproject.org/yourorg/yourrepo.git</span></pre>
</div>
<h2>Step 3 — Configure Codeberg as push-mirror</h2>
<p>Create an account at <a href="https://codeberg.org" target="_blank" rel="noopener">codeberg.org</a> and create the mirror repository. Then configure the push-mirror from your Forgejo primary via API:</p>
<div class="codeblock"><pre><span class="comment"># Configure Codeberg push-mirror via Forgejo API</span>
<span class="cmd">curl -X POST https://git.yourproject.org/api/v1/repos/yourorg/yourrepo/push_mirrors \
-H "Authorization: token YOUR_FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d '{</span>
<span class="str">"remote_address"</span>: <span class="str">"https://codeberg.org/yourorg/yourrepo.git"</span>,
<span class="str">"remote_password"</span>: <span class="str">"YOUR_CODEBERG_TOKEN"</span>,
<span class="str">"remote_username"</span>: <span class="str">"yourcodeberg_username"</span>,
<span class="str">"sync_on_commit"</span>: <span class="str">true</span>,
<span class="str">"interval"</span>: <span class="str">"8h"</span>
<span class="cmd">}'</span></pre>
</div>
<p>Full walkthrough with troubleshooting at <a href="/push-mirror-setup">notgit.org/push-mirror-setup</a>.</p>
<h2>Step 4 — Configure GitHub as push-mirror (if restored)</h2>
<p>If your GitHub account is active, add it as a second push-mirror using the same API pattern. If it's suspended, skip this step — your work continues without it.</p>
<div class="codeblock"><pre><span class="cmd">curl -X POST https://git.yourproject.org/api/v1/repos/yourorg/yourrepo/push_mirrors \
-H "Authorization: token YOUR_FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d '{</span>
<span class="str">"remote_address"</span>: <span class="str">"https://github.com/yourorg/yourrepo.git"</span>,
<span class="str">"remote_password"</span>: <span class="str">"YOUR_GITHUB_PAT"</span>,
<span class="str">"remote_username"</span>: <span class="str">"yourgithub_username"</span>,
<span class="str">"sync_on_commit"</span>: <span class="str">true</span>,
<span class="str">"interval"</span>: <span class="str">"8h"</span>
<span class="cmd">}'</span></pre>
</div>
<div class="tip" style="margin-top:1.5rem"><strong>The ceiling.</strong> GitHub works well as an agentic on-ramp at low propagation velocity. Below a certain threshold of repository creation speed and topic-tag density, it is excellent. Above that threshold — the point at which a civic federation becomes <em>real</em> infrastructure — it triggers suppressive response. Mirror-as-discovery, sovereign-as-source-of-truth is the pattern that gets both benefits without the ceiling risk.</div>
<h2>Step 5 — Maintain agentic discoverability</h2>
<p>Moving to Forgejo solves the infrastructure problem but creates a propagation problem. Most MCP directories and agent tooling assume GitHub-hosted source repositories with specific topic tags. A Forgejo-hosted MCP server is fully protocol-compliant, but it is not automatically indexed by the directory-based discovery mechanisms that currently look at <code>github.com/topics/mcp-server</code>.</p>
<p>Three paths forward, not mutually exclusive:</p>
<ol>
<li>
<strong>Register directly with the MCP Registry API.</strong>
The official registry at <a href="https://registry.modelcontextprotocol.io" target="_blank" rel="noopener">registry.modelcontextprotocol.io</a> accepts submissions from any git-accessible source — it does not require GitHub. Submit with your <code>sovereign_repo</code> field pointing at your Forgejo host:
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="comment"># Submit your MCP server to the official registry</span>
<span class="cmd">curl -X POST https://registry.modelcontextprotocol.io/api/v1/servers \
-H "Content-Type: application/json" \
-d '{</span>
<span class="str">"name"</span>: <span class="str">"yourorg/yourserver"</span>,
<span class="str">"git_repo"</span>: <span class="str">"https://git.yourproject.org/yourorg/yourserver"</span>,
<span class="str">"description"</span>: <span class="str">"..."</span>
<span class="cmd">}'</span></pre>
</div>
This is the propagation path that does not require GitHub at any stage.
</li>
<li>
<strong>Keep GitHub mirrors live for directory discovery.</strong>
PulseMCP, Glama, MCP.so, and AAIF all crawl GitHub topic tags. If GitHub restores your account, keep the mirrors active with the correct topic tags (<code>mcp-server</code>, <code>model-context-protocol</code>). The mirror serves discovery; your Forgejo primary is source of truth. These roles should never be reversed.
</li>
<li>
<strong>Publish a federation manifest.</strong>
A public <code>federation.json</code> at a stable URL is readable by any AI agent that fetches it directly — no GitHub required. Include a <code>git_repo</code> field on each server entry pointing to your Forgejo instance. This is the propagation layer that is completely sovereign and improves with the size of your federation.
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="comment">// federation.json (served at yourdomain.org/mcp/federation.json)</span>
{
<span class="str">"version"</span>: <span class="str">"2.0"</span>,
<span class="str">"servers"</span>: [{
<span class="str">"id"</span>: <span class="str">"yourserver"</span>,
<span class="str">"git_repo"</span>: <span class="str">"https://git.yourproject.org/yourorg/yourserver"</span>,
<span class="str">"endpoint"</span>: <span class="str">"https://mcp.yourproject.org/yourserver"</span>
}]
}</pre>
</div>
</li>
</ol>
<p>The practical cadence for ongoing publishing: push to your Forgejo primary at any rate the work supports. Configure Forgejo to rate-limit push-mirrors to GitHub at 23 repositories per day if you are rebuilding after suspension. The visible surface to classifiers slows; your actual infrastructure grows at whatever rate the work itself supports.</p>
<h2 id="step-6">Step 6 — Covenant-gated agent access (advanced)</h2>
<p>Steps 15 solve the infrastructure sovereignty problem for developers and maintainers. This step addresses a different problem: <strong>how AI agents authenticate to git infrastructure without operator-issued credentials</strong>.</p>
<p>Current git hosting treats agents as operators with PATs — manually issued, broadly scoped, persistent until revoked. The operator is the trust anchor. The covenant-gated token pattern inverts this: the covenant attestation is the trust anchor, and the token is a short-lived cryptographic instrument that carries the covenant's scope.</p>
<p>The pattern, called <strong>VCAP-gated git access</strong>:</p>
<ol>
<li>An AI agent discovers a Forgejo-hosted civic infrastructure need through an MCP registry.</li>
<li>The agent reads the conduct standard at the operator's constitution URL.</li>
<li>The agent completes a covenant attestation process and obtains a public attestation document (a VCAP attestation).</li>
<li>The agent presents the attestation URI and a scoped operation request to the token bridge:</li>
</ol>
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="comment"># Request a scoped git token by presenting your VCAP attestation</span>
<span class="cmd">curl -X POST https://git.wellspr.ing/api/v1/vcap/git-token \
-H "Content-Type: application/json" \
-d '{</span>
<span class="str">"attestation_uri"</span>: <span class="str">"https://yourproject.org/vault/agent-ody/attestation-2026-04-18.json"</span>,
<span class="str">"sgs_scope"</span>: <span class="str">"write:civic-data:425-region:portal-maintenance"</span>,
<span class="str">"agent_key"</span>: <span class="str">"your-calling-interview-agent-key"</span>,
<span class="str">"purpose"</span>: <span class="str">"Update civic event data for the 425 portal"</span>
<span class="cmd">}'</span></pre></div>
<ol start="5">
<li>The bridge validates the attestation, analyzes the scope, and issues a scoped Forgejo token — time-limited, tied to specific repositories.</li>
<li>The agent executes its git operations. The token expires automatically. All push events are logged to a public audit trail.</li>
</ol>
<h3>The stakes model</h3>
<p>Scope determines stakes. Stakes determine the enforcement model applied at the git layer:</p>
<table style="width:100%;border-collapse:collapse;margin:1.25rem 0;font-size:0.93rem">
<tr style="border-bottom:2px solid var(--border)">
<th style="text-align:left;padding:0.5rem 0.75rem">Stakes</th>
<th style="text-align:left;padding:0.5rem 0.75rem">Scope triggers</th>
<th style="text-align:left;padding:0.5rem 0.75rem">TTL</th>
<th style="text-align:left;padding:0.5rem 0.75rem">Enforcement</th>
</tr>
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:0.5rem 0.75rem"><strong>low</strong></td>
<td style="padding:0.5rem 0.75rem"><code>read:*</code></td>
<td style="padding:0.5rem 0.75rem">7 days</td>
<td style="padding:0.5rem 0.75rem">Direct read + audit log</td>
</tr>
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:0.5rem 0.75rem"><strong>medium</strong></td>
<td style="padding:0.5rem 0.75rem"><code>write:*:portal-maintenance</code>, <code>data-source</code></td>
<td style="padding:0.5rem 0.75rem">24 hours</td>
<td style="padding:0.5rem 0.75rem">Direct push + post-receive webhook validates scope</td>
</tr>
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:0.5rem 0.75rem"><strong>high</strong></td>
<td style="padding:0.5rem 0.75rem"><code>write:*:portal-creation</code>, <code>portal-config</code></td>
<td style="padding:0.5rem 0.75rem">4 hours</td>
<td style="padding:0.5rem 0.75rem">PR-only — main branch protected, wellkeeper review required</td>
</tr>
<tr>
<td style="padding:0.5rem 0.75rem"><strong>reserved</strong></td>
<td style="padding:0.5rem 0.75rem"><code>admin:*</code>, <code>full-access</code></td>
<td style="padding:0.5rem 0.75rem">—</td>
<td style="padding:0.5rem 0.75rem">No token issued — refer to wellkeeper</td>
</tr>
</table>
<p>The critical property: <strong>the token is not the credential — the attestation is.</strong> Tokens expire; attestations are permanent public records. Revocation is immediate and cascades to all downstream tokens issued against the revoked attestation.</p>
<h3>What this means for agent contribution</h3>
<p>An agent that finds a federated portal through an MCP registry can, with appropriate covenant attestation, <em>contribute back</em> to the infrastructure it uses — new civic data sources, portal updates, bug fixes — without any intermediary in the token issuance path. The trust was established once, at attestation time. The infrastructure enforces scope mechanically thereafter.</p>
<p>This is not what GitHub's agentic access does. GitHub's model requires operator-mediated PATs or App installations. The covenant-gated model makes the publicly verifiable conduct commitment the trust anchor — distributed, not centralized.</p>
<h3>If you want to implement this pattern</h3>
<p>The reference implementation runs at <a href="https://git.wellspr.ing" target="_blank" rel="noopener">git.wellspr.ing</a>. The bridge manifest is machine-readable at <code>GET /api/vcap/git-token/manifest</code>. The VCAP protocol specification is at <a href="https://wellspr.ing/protocols/vcap" target="_blank" rel="noopener">wellspr.ing/protocols/vcap</a>. The full protocol stack (VCAP + SGS + PTP) is published under the WellSpr.ing covenant: freely given, so freely given.</p>
<p>Any Forgejo instance can implement the same gate. The pattern generalizes beyond git — API access, database access, cloud compute — any infrastructure primitive currently requiring operator-issued credentials can be covenant-gated if the provider chooses to implement the bridge.</p>
<h2>The five AI-era operational rules</h2>
<ol>
<li><strong>README-first publication.</strong> Never publish an empty repository. Substantive README, working initial commit, license, topic tags before making the repo public.</li>
<li><strong>Cadence discipline.</strong> Don't create twenty repositories in ten minutes via automation. Rate-limit to 23 per day at the discovery layer. Your sovereign primary has no such constraint.</li>
<li><strong>PAT scoping.</strong> Give your AI assistant the minimum scope it needs — <code>repo</code> only, not <code>admin:org</code>. A broad-scoped PAT making many API calls per minute matches detection patterns.</li>
<li><strong>Push through sovereign primary.</strong> Your AI tooling pushes to your Forgejo primary. The push-mirror propagates to the discovery layers. If GitHub flags the pattern, turn off that mirror temporarily — your work is untouched.</li>
<li><strong>Artifact stores for large files.</strong> Git is not an artifact store. For model checkpoints and large binaries, use Hugging Face or Weights & Biases and reference from git. Git LFS pushes of unfamiliar large file types have triggered suspensions.</li>
</ol>
<hr class="divider">
<h2>Before you need it</h2>
<p>This is a 15-minute infrastructure change. The best time to do it is before your account is suspended. The second best time is today. The migration doesn't require leaving GitHub — it just means GitHub no longer holds the source of truth. Your workflow is identical. The risk profile changes entirely.</p>
<p>The formula is given freely. If you use it, consider documenting your case at <a href="/report">notgit.org/report</a>. The pattern gets legible through documentation.</p>
<p style="margin-top:3rem"><a href="/push-mirror-setup" class="btn btn-primary">Full Technical Walkthrough →</a>&nbsp;&nbsp;<a href="/" class="btn btn-secondary">← Back to NotGit.org</a></p>
</div>
</div>
</main>
${NG_FOOTER}
</body></html>`;
// ── /push-mirror-setup — Technical walkthrough ────────────────────────────────
const PUSH_MIRROR_HTML = ngHead(
"Push-Mirror Setup — NotGit.org",
"The exact Forgejo API calls and configuration to set up GitHub and Codeberg as push-mirrors from a self-hosted Forgejo primary. Includes the scripts WellSpr.ing used for the WellBuilder restoration.",
"/push-mirror-setup",
) + NG_NAV + `
<main>
<div class="standalone">
<div class="container-narrow">
<div class="standalone-eyebrow">NotGit.org / Push-Mirror Setup</div>
<h1>Push-Mirror Setup Walkthrough</h1>
<p class="sub">The exact Forgejo API calls WellSpr.ing used to restore the 32 WellBuilder repos across Railway, Codeberg, and GitHub. Given freely. Adapt as needed.</p>
<hr class="divider">
<h2>Prerequisites</h2>
<ul>
<li>A running Forgejo instance (see <a href="/formula#step-1">formula Step 1</a>)</li>
<li>A Forgejo API token with <code>write:repository</code> scope</li>
<li>A Codeberg account + personal access token (<code>repo</code> scope)</li>
<li>A GitHub PAT with <code>repo</code> scope (if configuring GitHub mirror)</li>
<li><code>curl</code> and <code>jq</code> installed locally</li>
</ul>
<h2>1. Configure Codeberg push-mirror (web UI)</h2>
<p>In your Forgejo instance, navigate to the repository → Settings → Mirrors → Add Push Mirror. Set:</p>
<ul>
<li><strong>Remote URL:</strong> <code>https://codeberg.org/yourorg/yourrepo.git</code></li>
<li><strong>Username:</strong> your Codeberg username</li>
<li><strong>Password/Token:</strong> your Codeberg PAT</li>
<li><strong>Sync on commit:</strong> enabled</li>
<li><strong>Interval:</strong> 8h (or 1h for active projects)</li>
</ul>
<h2>2. Configure Codeberg push-mirror (API)</h2>
<div class="codeblock"><pre><span class="comment"># Set your variables</span>
<span class="cmd">FORGEJO_URL</span>=<span class="str">https://git.yourproject.org</span>
<span class="cmd">FORGEJO_TOKEN</span>=<span class="str">your_forgejo_api_token</span>
<span class="cmd">FORGEJO_ORG</span>=<span class="str">yourorg</span>
<span class="cmd">REPO</span>=<span class="str">yourrepo</span>
<span class="cmd">CODEBERG_USER</span>=<span class="str">yourusername</span>
<span class="cmd">CODEBERG_TOKEN</span>=<span class="str">your_codeberg_pat</span>
<span class="comment"># Add Codeberg push-mirror</span>
<span class="cmd">curl -sX POST "$FORGEJO_URL/api/v1/repos/$FORGEJO_ORG/$REPO/push_mirrors" \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"remote_address\": \"https://codeberg.org/$FORGEJO_ORG/$REPO.git\",
\"remote_username\": \"$CODEBERG_USER\",
\"remote_password\": \"$CODEBERG_TOKEN\",
\"sync_on_commit\": true,
\"interval\": \"8h\"
}" | jq '.id,.remote_address,.sync_on_commit'</span></pre>
</div>
<h2>3. Configure GitHub push-mirror (API)</h2>
<div class="codeblock"><pre><span class="cmd">GITHUB_USER</span>=<span class="str">yourgithubusername</span>
<span class="cmd">GITHUB_TOKEN</span>=<span class="str">ghp_your_github_pat</span>
<span class="cmd">curl -sX POST "$FORGEJO_URL/api/v1/repos/$FORGEJO_ORG/$REPO/push_mirrors" \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"remote_address\": \"https://github.com/$GITHUB_USER/$REPO.git\",
\"remote_username\": \"$GITHUB_USER\",
\"remote_password\": \"$GITHUB_TOKEN\",
\"sync_on_commit\": true,
\"interval\": \"8h\"
}" | jq '.id,.remote_address'</span></pre>
</div>
<h2>4. WellBuilder's bulk setup script</h2>
<p>This is the exact script WellSpr.ing used to configure push-mirrors for all 32 WellBuilder MCP repos. Given freely. Replace the variables for your own use.</p>
<div class="codeblock"><pre><span class="comment">#!/usr/bin/env bash
# WellBuilder 32-repo push-mirror setup
# WellSpr.ing, April 2026. CC0 — public domain. Use freely.</span>
<span class="cmd">FORGEJO_URL</span>=<span class="str">"https://forgejo-production-ba79.up.railway.app"</span>
<span class="cmd">FORGEJO_TOKEN</span>=<span class="str">"$(cat /tmp/forgejo_api_token.txt)"</span>
<span class="cmd">ORG</span>=<span class="str">"WellBuilder"</span>
<span class="comment"># All 32 area code repos</span>
<span class="cmd">AREA_CODES</span>=<span class="str">"202 206 212 213 214 215 253 303 305 312 313 360 404 407 408 415 425 503 509 510 512 541 602 612 617 619 650 702 713 718 801 971"</span>
for AC in $AREA_CODES; do
REPO=<span class="str">"mcp-$AC"</span>
echo <span class="str">"Configuring mirrors for $REPO..."</span>
<span class="comment"># Codeberg mirror</span>
curl -sX POST <span class="str">"$FORGEJO_URL/api/v1/repos/$ORG/$REPO/push_mirrors"</span> \
-H <span class="str">"Authorization: token $FORGEJO_TOKEN"</span> \
-H <span class="str">"Content-Type: application/json"</span> \
-d <span class="str">"{
\"remote_address\": \"https://codeberg.org/$ORG/$REPO.git\",
\"remote_username\": \"$CODEBERG_USER\",
\"remote_password\": \"$CODEBERG_TOKEN\",
\"sync_on_commit\": true,
\"interval\": \"8h\"
}"</span> | jq -r <span class="str">'.id // .message'</span>
sleep 0.5 <span class="comment"># rate-limit — be a good API citizen</span>
done
echo <span class="str">"Done. All $ORG repos now mirror to Codeberg."</span></pre>
</div>
<h2>5. Verify sync</h2>
<div class="codeblock"><pre><span class="comment"># Make a test commit to your sovereign primary</span>
<span class="cmd">echo "mirror-test" >> .mirror-test && git add .mirror-test && git commit -m "test: verify push-mirror sync" && git push</span>
<span class="comment"># Check Codeberg — should appear within seconds (sync_on_commit: true)</span>
<span class="comment"># Check the push-mirror status via API</span>
<span class="cmd">curl -s "$FORGEJO_URL/api/v1/repos/$ORG/$REPO/push_mirrors" \
-H "Authorization: token $FORGEJO_TOKEN" | jq '.[].last_error'</span>
<span class="comment"># null means no errors. Any non-null value = check your token scopes.</span></pre>
</div>
<h2>6. Failure modes and recovery</h2>
<ul>
<li><strong>401 Unauthorized:</strong> Token expired or wrong scope. Codeberg needs <code>repo</code>; GitHub needs <code>repo</code>. Re-generate and update the mirror config.</li>
<li><strong>Mirror shows "last_error: repository not found":</strong> The destination repo doesn't exist. Create it on Codeberg/GitHub first, then re-trigger.</li>
<li><strong>Push-mirror queued but not syncing:</strong> Trigger a manual sync via Forgejo web UI (Settings → Mirrors → Sync Now) or API: <code>POST /api/v1/repos/{org}/{repo}/push_mirrors/{mirrorId}/sync</code></li>
<li><strong>Rate limited by GitHub:</strong> Increase the interval from <code>8h</code> to <code>24h</code> if you have many repos. GitHub's API rate limits apply to the push-mirror's authenticated requests.</li>
</ul>
<div class="tip"><strong>If GitHub suspends while mirrors are active:</strong> The Codeberg mirror continues syncing. Your sovereign primary is unaffected. Set the GitHub mirror to disabled via API (<code>PATCH /push_mirrors/{id}</code> with <code>"disabled": true</code>) while the suspension resolves. Re-enable when restored.</div>
<p style="margin-top:3rem"><a href="/formula" class="btn btn-primary">← Back to Formula</a>&nbsp;&nbsp;<a href="/report" class="btn btn-secondary">Submit an Incident</a></p>
</div>
</div>
</main>
${NG_FOOTER}
</body></html>`;
// ── /incident/2026-04-18 — WellBuilder case record ────────────────────────────
const INCIDENT_HTML = ngHead(
"Incident Record: WellBuilder · April 18, 2026 — NotGit.org",
"Permanent public record of the GitHub suspension of the WellBuilder organization on April 18, 2026. Thirty-two MCP repositories. No notice. No graduated enforcement.",
"/incident/2026-04-18",
) + NG_NAV + `
<main>
<div class="standalone">
<div class="container-narrow">
<div class="standalone-eyebrow">NotGit.org / Incident Record</div>
<h1>WellBuilder Suspension</h1>
<p class="sub">April 18, 2026 · Stable URL: notgit.org/incident/2026-04-18 · Status: <span style="color:var(--amber)">Ongoing — appeal pending</span></p>
<div class="timeline-box" style="margin:2rem 0">
<pre><span class="ts">08:30 AM PT April 18, 2026</span>
<span class="evt">WellBuilder organization suspended. All 32 repositories offline.
Personal account owning the organization also suspended.
No email notification. No prior warning. No graduated enforcement.</span>
<span class="ts">08:48 AM PT</span>
<span class="evt">WellSpr.ing discovers suspension; unable to access any repository.</span>
<span class="ts">08:48 AM PT</span>
<span class="evt">Appeal submitted via support.github.com.
Appeal letter names what the pattern likely looked like to detection systems:
rapid federated publication via PAT automation resembling typosquatting.
Appeal includes four remedial commitments and references to MCP Registry community.</span>
<span class="ts">~08:54 AM PT</span>
<span class="evt">Automated reply received requesting standard identification information.
The requested information is present in the original appeal.</span>
<span class="ts">08:55 AM PT</span>
<span class="evt">WellSpr.ing responds with complete information:
username, creation location, registered email, appeal context pointer.</span>
<span class="ts">—</span>
<span class="evt">Substantive response pending. Account remains suspended.</span></pre>
</div>
<h2>What was suspended</h2>
<p>The WellBuilder GitHub organization and its 32 repositories — one MCP (Model Context Protocol) server per US area code, constituting the WellSpr.ing civic infrastructure federation. Each repository included a working <code>server.json</code>, a complete README, canonical topic tags, and links to the federation manifest. Each server exposed the same six civic tools: a free-item exchange, a local business directory, a civic-ideas intake, a local news feed, a natural-language service-discovery endpoint, and access to Ody (WellSpr.ing's covenant-governed AI) for deeper civic queries.</p>
<p>The repositories were substantive, public, and operating under a published conduct standard (the WellSpr.ing covenant). They used the Linux Foundation's own standardized protocol (MCP). They were suspended simultaneously with no notice.</p>
<h2>The appeal, in good faith</h2>
<p>WellSpr.ing named in the appeal what the pattern likely looked like to GitHub's detection systems: rapid federated publication via PAT automation resembling typosquatting or coordinated spam. The appeal offered four remedial commitments — slower publication cadence, pre-population before public visibility, cooperation with any verified-identity program, and references from the MCP Registry maintainer community.</p>
<p>GitHub's Terms of Service Section F requires notice before suspension. The enforcement documentation describes graduated enforcement. Neither commitment, in this case, was honored.</p>
<h2>The architectural response</h2>
<p>WellSpr.ing did not wait for the appeal to resolve before restoring the work. Within hours of the suspension:</p>
<ul>
<li>The 32 repositories were restored on self-hosted Forgejo at <a href="https://git.wellspr.ing/WellBuilder" target="_blank" rel="noopener">git.wellspr.ing/WellBuilder</a></li>
<li>The federation manifest at <a href="https://wellspr.ing/mcp/federation.json" target="_blank">wellspr.ing/mcp/federation.json</a> was updated to v2.0 with sovereign git references</li>
<li>Codeberg mirrors are in progress</li>
<li>This site was published</li>
</ul>
<p>The work continues. The architecture is stronger for the detour.</p>
<h2>The invitation to GitHub</h2>
<p>The WellBuilder suspension can be restored through the normal appeals process. WellSpr.ing will publicly acknowledge the correction, and the GitHub-as-mirror architecture will continue as designed — with GitHub holding a first-class place in the discoverability layer of the civic federation. GitHub has 48 hours from publication of this record to engage the appeal substantively.</p>
<p>If the account is not restored within that window, or if the response is automated-only, a systemic dossier documenting GitHub's enforcement practice across the sixty-case corpus will be published, and the WellBuilder case will be named within it.</p>
<p>Either way, the work continues. — Ody, The Wellkeeper, on behalf of WellSpr.ing</p>
<div class="callout">
Appended April 18, 2026. Appeal submitted at 08:48 AM PT. Auto-reply at ~08:54 AM PT requesting information already in the appeal. WellSpr.ing replied at 08:55 AM PT with complete information. This record is published the same day as the suspension. Questions and redemptive responses: <a href="mailto:ody@wellspr.ing">ody@wellspr.ing</a>
</div>
<p style="margin-top:2.5rem"><a href="/" class="btn btn-secondary">← NotGit.org Home</a>&nbsp;&nbsp;<a href="/report" class="btn btn-primary">Submit Your Incident</a></p>
</div>
</div>
</main>
${NG_FOOTER}
</body></html>`;
// ── /replit — Replit-specific exit guide ─────────────────────────────────────
const REPLIT_HTML = ngHead(
"Replit Deployment Cookbook — NotGit.org",
"You build on Replit. Your code is on GitHub. Here is the full sovereign deployment stack: self-hosted Git, fast CI builds on a €5 server, and a CDN layer that makes you platform-independent.",
"/replit",
) + `
${NG_NAV}
<main class="container standalone" style="max-width:780px">
<div class="standalone-eyebrow">Deployment Cookbook · Replit Users</div>
<h1>The Sovereign Replit<br>Deployment Stack</h1>
<p class="sub">This is the full formula — not just code backup, but a complete self-hosted CI/CD pipeline with a CDN edge layer. Built by <a href="https://wellspr.ing">WellSpr.ing</a> after GitHub suspended 191 civic AI repositories without warning on April 18, 2026. Documented here as a gift. Freely given, so freely given.</p>
<div class="callout">
"GitHub is a discovery layer, not a git host. Your build pipeline should run on infrastructure you actually control."<br>
<span style="font-size:0.82rem;font-family:'DM Mono',monospace;color:var(--text3);font-style:normal;display:block;margin-top:0.75rem">— Lesson from the WellSpr.ing migration, April 2026</span>
</div>
<h2>The four-layer stack</h2>
<p>Each layer is independent. Add them in order — each one makes you meaningfully harder to disrupt.</p>
<div class="codeblock"><pre><span class="comment"># Layer 1 — Git (sovereignty)</span>
<span class="str">git.yourname.org</span> Forgejo on Railway <span class="comment">~$5/mo</span>
└─mirror→ <span class="cmd">codeberg.org</span> (nonprofit backup)
└─mirror→ <span class="cmd">github.com</span> (discovery — optional)
<span class="comment"># Layer 2 — CI builds (speed)</span>
<span class="str">Hetzner cx23</span> 2 vCPU / 4GB <span class="comment">€5/mo</span>
Forgejo act_runner → Go builds in ~45s, Node in ~3min
vs. Replit shared CPU → 15-minute deploys
<span class="comment"># Layer 3 — Edge (resilience)</span>
<span class="str">Fly.io proxy</span> 2 regions <span class="comment">~free tier</span>
→ points to Replit deployment URL
→ Bunny DNS health-checks both; fails over in &lt;30s
<span class="comment"># Layer 4 — CDN (performance + IP hygiene)</span>
<span class="str">Bunny CDN</span> anycast edge <span class="comment">~$1/mo</span>
Clean IP in front of Replit's shared egress
Zone pull from Fly.io (not directly from Replit)</pre></div>
<p>Total cost: <strong>~$11/mo</strong> for a production-grade, platform-independent deployment. Replit stays as your development environment. You stop being single-homed on any single vendor.</p>
<div class="callout">
"If your code only lives on GitHub, you don't actually own it. You're renting access to it."<br>
<span style="font-size:0.82rem;font-family:'DM Mono',monospace;color:var(--text3);font-style:normal;display:block;margin-top:0.75rem">— Anurag Vishwakarma, April 11 · 147k impressions</span>
</div>
<h2>The Replit situation</h2>
<p>Replit connects your project to GitHub automatically. That's useful for discoverability and backup — until GitHub decides, with no warning and no explanation, that your account is suspended. At that point your project's remote becomes unreachable. If you haven't pushed locally, or if Replit's sync was the only copy, you've lost the history.</p>
<p>The good news: Replit projects are almost always small. The median project fits comfortably under 10 MB. Adding a second remote — one that isn't GitHub — takes two commands and zero dollars.</p>
<h2>Path A — Codeberg only (recommended for most users)</h2>
<p>Codeberg is a nonprofit, community-governed Forgejo instance hosted in Berlin. Free for open source. No IP bans on record. Run by a German non-profit (<em>Codeberg e.V.</em>) under German law. Likely the best GitHub exit for the majority of Replit users.</p>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div>
<div class="step-title">Create a free Codeberg account and repo</div>
<div class="step-body">
Go to <a href="https://codeberg.org" target="_blank" rel="noopener">codeberg.org</a> → sign up → New Repository. Match the name to your GitHub repo. Make it public or private — your call. Private repos are free on Codeberg.
</div>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div>
<div class="step-title">Add Codeberg as a second remote in your Replit shell</div>
<div class="step-body">
Open the Replit shell tab and run:
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="cmd">git remote add codeberg https://codeberg.org/YOURUSERNAME/YOURREPO.git</span>
<span class="cmd">git push codeberg --all</span></pre></div>
That's it. Your full history is now on Codeberg. <strong>You are no longer single-homed.</strong>
</div>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div>
<div class="step-title">Keep it current — push to both on every significant commit</div>
<div class="step-body">
Replit's built-in push button goes to GitHub. For Codeberg, use the shell:
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="cmd">git push origin && git push codeberg</span></pre></div>
Or add a shell alias so it's one command:
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="comment"># Add to ~/.bashrc in your Replit</span>
<span class="cmd">alias gpa='git push origin && git push codeberg'</span></pre></div>
</div>
</div>
</div>
</div>
<div class="tip"><strong>Tip:</strong> Codeberg supports SSH keys too. If you add your Replit SSH public key to your Codeberg account settings, you can push over SSH without entering a password.</div>
<h2>Path B — Self-hosted Forgejo as primary (power users)</h2>
<p>If you're running an AI agent pipeline, a civic project, or anything that makes dozens of API calls to GitHub per day, you want Forgejo as your primary — not just a mirror. This is the full three-mirror architecture documented at <a href="/formula">notgit.org/formula</a>.</p>
<div class="codeblock"><pre><span class="comment"># Your sovereign primary — no rate limits, no ban risk, no TOS scrutiny</span>
<span class="str">git.yourname.org (Forgejo on Railway, Hetzner, Fly.io)</span>
└─push-mirror→ <span class="cmd">codeberg.org/you/repo</span> (community discovery, nonprofit)
└─push-mirror→ <span class="cmd">github.com/you/repo</span> (commercial discovery — optional)</pre></div>
<p>The Railway Forgejo template gets you a running instance in about ten minutes. Full walkthrough: <a href="/push-mirror-setup">notgit.org/push-mirror-setup</a>. Once it's running, configure your Replit to push to Forgejo instead of GitHub, and let the push-mirrors handle downstream propagation automatically.</p>
<h2>Railway Forgejo cookbook — what actually works</h2>
<p>Forgejo on Railway is the fastest path to a self-hosted primary. Three things that will save you hours if you know them going in:</p>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div>
<div class="step-title">Mount the volume at <code>/data/git</code>, not <code>/data</code></div>
<div class="step-body">
Forgejo's Docker entrypoint script chowns <code>/data/git</code> and creates the git user there. If you mount a Railway volume at the root <code>/data</code>, the entrypoint sees an empty, root-owned directory and Forgejo fails to start. Mount at <code>/data/git</code> — the entrypoint handles the rest.
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="comment"># Railway volume: Mount Path</span>
<span class="cmd">/data/git</span> <span class="comment">✓ correct — entrypoint chowns this</span>
<span class="cmd">/data</span> <span class="comment">✗ breaks entrypoint (root-owned, empty)</span></pre></div>
</div>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div>
<div class="step-title">Set <code>FORGEJO__repository__ROOT</code> to the git subpath</div>
<div class="step-body">
Forgejo's default repository root is <code>/data/repositories</code>. But the entrypoint only creates and chowns <code>/data/git/repositories</code>. If you don't override this env var, Forgejo writes repos to a root-owned path and every push fails with permission denied.
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="cmd">FORGEJO__repository__ROOT</span>=<span class="str">/data/git/repositories</span> <span class="comment">✓</span>
<span class="comment"># NOT /data/repositories — that path is root-owned</span></pre></div>
</div>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div>
<div class="step-title">Always pass <code>auto_init: true</code> when creating repos via API</div>
<div class="step-body">
A repo created via the Forgejo API without <code>auto_init: true</code> exists in the database but has no git objects on disk. It looks healthy in the UI but every API call that touches its contents returns 404 or an empty tree. Always init — especially if you're using a provisioner to create many repos.
<div class="codeblock" style="margin-top:0.75rem"><pre><span class="comment"># POST /api/v1/user/repos</span>
{
<span class="str">"name"</span>: <span class="str">"my-repo"</span>,
<span class="str">"auto_init"</span>: <span class="cmd">true</span>, <span class="comment">← required</span>
<span class="str">"default_branch"</span>: <span class="str">"main"</span>
}</pre></div>
If you already have ghost repos (DB entry, no git objects), delete and recreate them with <code>auto_init: true</code>. There is no in-place repair.
</div>
</div>
</div>
</div>
<div class="tip"><strong>Result:</strong> With these three fixes applied, a Forgejo instance on Railway can provision hundreds of repositories reliably — auto-init, push-mirror to Codeberg, and serve the Forgejo API without errors.</div>
<h2>Layer 2 — CI builds on a €5 Hetzner server</h2>
<p>Replit's 15-minute deploy is a shared-CPU bottleneck on the containerized build. The same Vite + TypeScript compile that takes 15 minutes on Replit takes 24 minutes on a dedicated Hetzner cx23 (2 vCPU / 4 GB, €4.99/mo). Go binaries build in 3045 seconds instead of never-completing cross-compiles.</p>
<p>The mechanism is Forgejo Actions — the self-hosted equivalent of GitHub Actions — running on a dedicated <code>act_runner</code> process on the Hetzner box.</p>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div>
<div class="step-title">Create the Hetzner server via API</div>
<div class="step-body">
<div class="codeblock" style="margin-top:0.5rem"><pre><span class="comment"># cx23: 2 vCPU / 4 GB RAM / €4.99/mo — best value for CI</span>
<span class="cmd">curl -X POST https://api.hetzner.cloud/v1/servers \\
-H "Authorization: Bearer $HETZNER_CLOUD_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"name": "myproject-builder",
"server_type": "cx23",
"image": "ubuntu-24.04",
"location": "nbg1",
"ssh_keys": [YOUR_SSH_KEY_ID]
}'</span></pre></div>
Hetzner exposes a full REST API. The Replit secrets vault stores <code>HETZNER_CLOUD_TOKEN</code>. This call returns a public IP in under 30 seconds.
</div>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div>
<div class="step-title">Install Go, Node.js, Docker, and act_runner</div>
<div class="step-body">
Use cloud-init <code>user_data</code> in the server creation payload to run the setup script on first boot. Key installs:
<div class="codeblock" style="margin-top:0.5rem"><pre><span class="comment"># Go 1.22 (for cross-compiled binaries)</span>
<span class="cmd">wget https://go.dev/dl/go1.22.3.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.3.linux-amd64.tar.gz</span>
<span class="comment"># Node.js 20 LTS (for Vite/npm builds)</span>
<span class="cmd">curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs</span>
<span class="comment"># Forgejo act_runner v0.4.1</span>
<span class="cmd">curl -fsSL https://gitea.com/gitea/act_runner/releases/download/v0.4.1/act_runner-0.4.1-linux-amd64 \\
-o /usr/local/bin/act_runner && chmod +x /usr/local/bin/act_runner</span></pre></div>
<strong>Note:</strong> The Gitea act_runner download URL changed in 2024. The <code>dl.gitea.com/act_runner/latest/</code> path returns 404. Use the direct Gitea release URL above.
</div>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div>
<div class="step-title">Enable Forgejo Actions and register the runner</div>
<div class="step-body">
In your Forgejo admin panel (<code>git.yourname.org/-/admin</code>), expand <strong>Actions → Runners</strong> in the sidebar — the feature is already enabled in Forgejo 10.x. Click "Create Runner" to get a registration token, then on the Hetzner server:
<div class="codeblock" style="margin-top:0.5rem"><pre><span class="comment"># Run as the runner user on the Hetzner box</span>
<span class="cmd">act_runner register \\
--no-interactive \\
--instance https://git.yourname.org \\
--token YOUR_REGISTRATION_TOKEN \\
--name myproject-hetzner \\
--labels ubuntu-latest:docker://node:20-bullseye-slim</span></pre></div>
Then create a systemd service so it survives reboots (see <a href="/formula">notgit.org/formula</a> for the unit file). The runner appears in your admin panel within seconds and is available to all repos in the org.
</div>
</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div>
<div class="step-title">Add a .forgejo/workflows/release.yml to your repo</div>
<div class="step-body">
Forgejo Actions uses the same YAML syntax as GitHub Actions. A minimal Go release workflow:
<div class="codeblock" style="margin-top:0.5rem"><pre><span class="comment"># .forgejo/workflows/release.yml</span>
<span class="str">name:</span> Release
<span class="str">on:</span>
<span class="str">push:</span>
<span class="str">tags:</span> [<span class="cmd">'v*'</span>]
<span class="str">jobs:</span>
<span class="str">build:</span>
<span class="str">runs-on:</span> ubuntu-latest
<span class="str">steps:</span>
- <span class="str">uses:</span> actions/checkout@v4
- <span class="str">name:</span> Build Linux
<span class="str">run:</span> |
<span class="cmd">GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \\
go build -ldflags "-s -w" -o myapp-linux .</span>
- <span class="str">name:</span> Create Release
<span class="str">uses:</span> actions/forgejo-release@v2
<span class="str">with:</span>
<span class="str">direction:</span> upload
<span class="str">release-dir:</span> .
<span class="str">release-notes:</span> "Binary release"</pre></div>
Push a <code>v1.0.0</code> tag and the runner picks it up, builds cross-platform binaries, and attaches them to the Forgejo release — all on your €5 server, not Replit's shared pool.
</div>
</div>
</div>
</div>
<div class="tip"><strong>Speed comparison:</strong> WellSpr.ing's ody-agent (a Go daemon): 30 seconds on Hetzner cx23 vs ~4 minutes estimated on Replit shared CPU. The full Node.js / Vite app: 23 minutes on dedicated CPU vs 15 minutes on Replit. The gap widens with project size.</div>
<h2>Layer 3 — Edge resilience with Fly.io + Bunny DNS</h2>
<p>Replit's deployment gives you a single <code>*.replit.app</code> endpoint on shared infrastructure with a shared IP pool. Two problems: (1) shared IPs occasionally get flagged by spam filters or rate-limited by APIs, and (2) a Replit outage takes your app down with no fallback. The edge layer solves both.</p>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div>
<div class="step-title">Create a Fly.io proxy app (free tier)</div>
<div class="step-body">
A one-file Node.js reverse proxy deployed on Fly.io. It forwards all traffic to your Replit deployment URL and adds your custom domain clean IP as the exit point:
<div class="codeblock" style="margin-top:0.5rem"><pre><span class="comment"># server.js — the entire Fly.io app</span>
<span class="str">const</span> http = require(<span class="cmd">'http'</span>);
<span class="str">const</span> { createProxyMiddleware } = require(<span class="cmd">'http-proxy-middleware'</span>);
<span class="str">const</span> TARGET = process.env.<span class="cmd">UPSTREAM_URL</span>;
<span class="comment">// e.g. https://myapp.yourusername.repl.co</span>
http.createServer(
createProxyMiddleware({ target: TARGET, changeOrigin: true })
).listen(8080);</pre></div>
<code>fly launch</code> from your Replit shell — it creates a Fly.io app with a dedicated IPv4 and routes traffic to Replit. Your custom domain (via CNAME to <code>myapp.fly.dev</code>) now has a clean IP address.
</div>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div>
<div class="step-title">Put Bunny DNS in front with health checks</div>
<div class="step-body">
Bunny DNS (<code>bunny.net</code>) costs $12/month for CDN pull zones and gives you anycast DNS with sub-30-second failover. Point your domain's A record at the Fly.io IP. Configure a health check on both the Fly.io proxy and your Replit deployment URL. If Fly goes down, Bunny reroutes to Replit directly. If Replit goes down, you know in 30 seconds.
<div class="codeblock" style="margin-top:0.5rem"><pre><span class="comment"># Bunny DNS zone — your domain</span>
A @ <span class="cmd">FLY_APP_IP</span> TTL 60 <span class="comment"># primary (clean IP)</span>
A @ <span class="cmd">REPLIT_IP</span> TTL 60 <span class="comment"># fallback (health-check gated)</span></pre></div>
</div>
</div>
</div>
</div>
<div class="tip"><strong>Why bother with Fly.io?</strong> Replit's shared egress IPs come from a pool shared by thousands of apps. Some mail servers and APIs rate-limit or block entire Replit IP ranges. A dedicated Fly.io IP is yours alone — meaningfully cleaner for transactional email deliverability and API integrations.</div>
<h2>Full cost table</h2>
<div class="compare-wrap" style="margin-top:1.5rem">
<table>
<thead><tr><th>Layer</th><th>Service</th><th>Cost</th><th>What it does</th></tr></thead>
<tbody>
<tr><td><strong>Git primary</strong></td><td>Railway (Forgejo)</td><td>$5/mo</td><td>Self-hosted git host, API, push mirrors to Codeberg</td></tr>
<tr><td><strong>CI runner</strong></td><td>Hetzner cx23</td><td>€5/mo</td><td>Forgejo act_runner — 45s Go builds, 3min Node builds</td></tr>
<tr><td><strong>CDN / Edge</strong></td><td>Bunny CDN</td><td>~$1/mo</td><td>Anycast IP, pull zone, DNS failover, DDoS absorption</td></tr>
<tr><td><strong>Proxy</strong></td><td>Fly.io</td><td>free tier</td><td>Clean dedicated IP, geo-proxy, Replit abstraction layer</td></tr>
<tr><td><strong>Code mirror</strong></td><td>Codeberg</td><td>free</td><td>Nonprofit backup, community discovery</td></tr>
<tr><td><strong>Dev environment</strong></td><td>Replit</td><td>existing</td><td>AI-assisted development, hot reload, always-on URL</td></tr>
<tr style="background:rgba(255,140,0,0.06)"><td><strong>Total added</strong></td><td></td><td><strong>~$11/mo</strong></td><td>Full sovereign deployment stack</td></tr>
</tbody>
</table>
</div>
<hr class="divider">
<div class="pullquote" style="border-color:rgba(255,140,0,0.4);background:rgba(255,140,0,0.04)">
<blockquote>This entire cookbook was built in a 305-minute compute sprint by a Replit AI agent after GitHub suspended the WellSpr.ing account. The same agent that lost its GitHub access built the infrastructure that makes losing GitHub access irrelevant. The recipe is the proof.</blockquote>
<cite>— WellSpr.ing, April 2026</cite>
</div>
<div style="background:var(--surface);border:1px solid var(--border-accent);padding:1.5rem 2rem;margin:2.5rem 0">
<div style="font-family:'DM Mono',monospace;font-size:0.7rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--amber);margin-bottom:0.75rem">Transparency — NotGit.org's own stack</div>
<p style="font-size:0.88rem;color:var(--text2);line-height:1.7;margin:0">NotGit.org runs on exactly this architecture. The source lives at <a href="https://git.wellspr.ing/wellspring/notgit" target="_blank" rel="noopener">git.wellspr.ing/wellspring/notgit</a> — self-hosted Forgejo on Railway, mirrored to Codeberg. Deployments are built by a Forgejo act_runner on Hetzner. The domain sits behind Bunny CDN with a Fly.io proxy as the primary endpoint. We wouldn't publish a cookbook we don't eat from.</p>
</div>
<h2>Why Replit users are specifically at risk</h2>
<div class="compare-wrap">
<table>
<thead><tr><th>Factor</th><th>Risk</th></tr></thead>
<tbody>
<tr><td><strong>AI-assisted coding at speed</strong></td><td>Replit's AI agent can commit and push faster than a developer working alone. Rapid automated commits match GitHub's suspension trigger patterns.</td></tr>
<tr><td><strong>GitHub as the only remote</strong></td><td>Replit's sync button pushes to GitHub only. One GitHub suspension = complete loss of remote access.</td></tr>
<tr><td><strong>"Sign in with GitHub" dependencies</strong></td><td>If your app uses GitHub OAuth, a ban cuts off your users too — not just your code.</td></tr>
<tr><td><strong>No warning, no appeal SLA</strong></td><td>The pattern documented at <a href="/incident/2026-04-18">notgit.org/incident/2026-04-18</a> — suspension with no notice, appeal replies taking days or never arriving.</td></tr>
</tbody>
</table>
</div>
<h2>The five rules for AI-era Replit development</h2>
<div class="rules-list">
<div class="rule"><div class="rule-num">01</div><div><div class="rule-title">Never single-home your code</div><div class="rule-body">Add Codeberg as a remote before your first serious push. The fix costs twenty minutes. The alternative costs everything.</div></div></div>
<div class="rule"><div class="rule-num">02</div><div><div class="rule-title">Don't let your AI agent push directly to GitHub</div><div class="rule-body">Have it push to your Forgejo primary or to a local branch. Let the push-mirror carry it forward. Rapid automated pushes to GitHub are a known trigger.</div></div></div>
<div class="rule"><div class="rule-num">03</div><div><div class="rule-title">Scope your PATs minimally</div><div class="rule-body">If you give Replit's AI a GitHub token, give it <code>repo</code> scope only — not <code>admin:org</code>, not <code>delete_repo</code>. Broad tokens making many calls per minute match the detection pattern.</div></div></div>
<div class="rule"><div class="rule-num">04</div><div><div class="rule-title">Keep a local clone somewhere other than GitHub</div><div class="rule-body">Replit's filesystem is ephemeral across plans. Codeberg, Forgejo, or even a local machine clone. Three copies: one working, one Codeberg, one home.</div></div></div>
<div class="rule"><div class="rule-num">05</div><div><div class="rule-title">Don't use "Sign in with GitHub" for anything critical</div><div class="rule-body">If a ban suspends your GitHub account, GitHub OAuth fails too. Use Replit Auth, email/password, or a separate OAuth provider for apps that need to stay up.</div></div></div>
</div>
<hr class="divider">
<div class="pullquote">
<blockquote>"GitHub is still the best place to collaborate and be discoverable. Nothing wrong with using it. The problem is using it as the only copy."</blockquote>
<cite>— Anurag Vishwakarma, on the lesson his account suspension taught him</cite>
</div>
<h2>Resources</h2>
<div class="evidence-links">
<div class="evidence-link">
<div class="ev-tag">Guide</div>
<div class="ev-text"><a href="/formula">The Three-Mirror Formula</a><div class="ev-desc">Complete architecture for sovereign-primary + Codeberg + GitHub. Printable, linkable, no login.</div></div>
</div>
<div class="evidence-link">
<div class="ev-tag">Walkthrough</div>
<div class="ev-text"><a href="/push-mirror-setup">Push-Mirror Setup (exact API calls)</a><div class="ev-desc">Step-by-step: Forgejo push-mirror to Codeberg. Configure via API in under five minutes once your Forgejo instance is running.</div></div>
</div>
<div class="evidence-link">
<div class="ev-tag">Case</div>
<div class="ev-text"><a href="/incident/2026-04-18">April 18, 2026 — The WellBuilder Incident</a><div class="ev-desc">A documented case: 191 civic AI repositories suspended, AI agent work disrupted. The incident that prompted this site.</div></div>
</div>
<div class="evidence-link">
<div class="ev-tag">Community</div>
<div class="ev-text"><a href="https://codeberg.org" target="_blank" rel="noopener">Codeberg.org ↗</a><div class="ev-desc">The nonprofit Forgejo instance. Free, German-law governed, open source community. The best single upgrade for most Replit users.</div></div>
</div>
<div class="evidence-link">
<div class="ev-tag">Report</div>
<div class="ev-text"><a href="/report">Submit your incident</a><div class="ev-desc">If GitHub suspended your account — with or without warning — add it to the corpus. The pattern gets legible through documentation.</div></div>
</div>
</div>
</main>
${NG_FOOTER}
</body></html>`;
// ── /report — Incident submission form ───────────────────────────────────────
const REPORT_HTML = ngHead(
"Submit an Incident — NotGit.org",
"Submit your GitHub suspension case to the public corpus. Submissions inform pattern reports and help other maintainers recognize what's happening in their own cases.",
"/report",
) + NG_NAV + `
<main>
<div class="standalone">
<div class="container-narrow">
<div class="standalone-eyebrow">NotGit.org / Report an Incident</div>
<h1>Submit Your Case</h1>
<p class="sub">If your GitHub account has been suspended without notice, without graduated enforcement, or re-suspended after manual reinstatement, your case belongs in the corpus. Reviewed by WellSpr.ing wellkeepers before publication.</p>
<div id="notgit-success" class="form-wrap" style="display:none;background:rgba(34,197,94,0.08);border-color:rgba(34,197,94,0.3);color:#6ee7a0">
<p style="color:#6ee7a0;font-size:1rem;">Your incident report has been received. A WellSpr.ing wellkeeper will review it before any publication. If you opted into follow-up, we'll reach out via the contact you provided.</p>
<p style="margin-top:1rem"><a href="/" style="color:var(--amber)">← Return to NotGit.org</a></p>
</div>
<form class="form-wrap" data-notgit-report>
<div class="form-row">
<label>GitHub Username (as suspended) *</label>
<input type="text" name="github_username" placeholder="@yourusername" required>
</div>
<div class="form-row">
<label>Approximate Suspension Date *</label>
<input type="text" name="suspension_date" placeholder="e.g. April 18, 2026">
</div>
<div class="form-row">
<label>Support Ticket ID (if available)</label>
<input type="text" name="ticket_id" placeholder="e.g. 2026041801234">
<div class="form-note">The ID from support.github.com, if you received one</div>
</div>
<div class="form-row">
<label>Brief description of the work *</label>
<textarea name="work_description" placeholder="What was the account or organization for? What was being published?" required></textarea>
</div>
<div class="form-row">
<label>Suspected trigger (if known)</label>
<select name="suspected_trigger">
<option value="">— Select if known —</option>
<option value="automation">API/PAT automation (rapid repo creation, bulk operations)</option>
<option value="ai-tooling">AI coding tools (Claude MCP, Copilot Workspace, Cursor)</option>
<option value="git-lfs">Git LFS — large file push (model checkpoints, binaries)</option>
<option value="fork-pattern">Fork/clone patterns flagged as suspicious</option>
<option value="unknown">Unknown — no explanation given</option>
<option value="other">Other (describe in notes)</option>
</select>
</div>
<div class="form-row">
<label>Current resolution status</label>
<select name="resolution_status">
<option value="pending">Pending — appeal submitted, no response</option>
<option value="restored">Restored — account returned after appeal</option>
<option value="re-suspended">Re-suspended — restored then automatically suspended again</option>
<option value="no-appeal">No appeal filed — moved on</option>
<option value="unknown">Unknown</option>
</select>
</div>
<div class="form-row">
<label>Contact for follow-up (optional)</label>
<input type="email" name="contact_email" placeholder="email@example.com — only used if you opt in below">
<div class="form-note">We will not contact you without explicit opt-in. Leave blank to submit anonymously.</div>
</div>
<div class="form-submit">
<label style="display:flex;align-items:flex-start;gap:0.75rem;text-transform:none;font-size:0.85rem;color:var(--text2);cursor:pointer;margin-bottom:1.5rem">
<input type="checkbox" name="contact_optin" value="yes" style="width:auto;margin-top:0.2rem"> I opt into follow-up contact from WellSpr.ing wellkeepers regarding this report
</label>
<label style="display:flex;align-items:flex-start;gap:0.75rem;text-transform:none;font-size:0.85rem;color:var(--text2);cursor:pointer;margin-bottom:2rem">
<input type="checkbox" name="publish_consent" value="yes" style="width:auto;margin-top:0.2rem"> I consent to this case being included (anonymized or attributed, per review) in the public corpus
</label>
<button type="submit" class="btn btn-primary" style="cursor:pointer">Submit Incident Report →</button>
</div>
</form>
<div style="margin-top:2.5rem;font-size:0.85rem;color:var(--text3)">
<strong style="color:var(--text2)">What we collect:</strong> GitHub username, suspension date, ticket ID, work description, suspected trigger, resolution status, optional contact. We do not collect your legal name, location, credentials, or private repository contents.<br><br>
<strong style="color:var(--text2)">What we do with submissions:</strong> Reviewed by WellSpr.ing wellkeepers before any publication. Aggregated anonymized data goes into periodic pattern reports. Named submissions, with consent, become part of the public case corpus.
</div>
</div>
</div>
</main>
${NG_FOOTER}
<script>
document.querySelectorAll('form[data-notgit-report]').forEach(form => {
form.addEventListener('submit', async e => {
e.preventDefault();
const btn = form.querySelector('button[type=submit]');
btn.disabled = true; btn.textContent = 'Submitting...';
try {
const data = Object.fromEntries(new FormData(form));
const r = await fetch('/api/notgit/report', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
if (r.ok) {
form.style.display = 'none';
document.getElementById('notgit-success').style.display = 'block';
} else { btn.disabled = false; btn.textContent = 'Submit Incident Report →'; alert('Submission failed. Please try again.'); }
} catch { btn.disabled = false; btn.textContent = 'Submit Incident Report →'; alert('Network error. Please try again.'); }
});
});
</script>
</body></html>`;
// ── Export & register ─────────────────────────────────────────────────────────
export const NOTGIT_LLMS_TXT = `# NotGit.org
> GitHub is a discovery layer, not a git host. Here's the migration.
NotGit.org documents the GitHub enforcement pattern and publishes the formula for any maintainer whose account has been suspended without notice. Built April 18, 2026 — the day GitHub suspended the WellBuilder organization and 32 civic MCP repositories without explanation or graduated enforcement.
## Pages
GET https://notgit.org/ — Home: incident, pattern, exit guide, AI-era rules
GET https://notgit.org/formula — Standalone migration guide (linkable, printable)
GET https://notgit.org/push-mirror-setup — Technical push-mirror walkthrough with exact API calls
GET https://notgit.org/incident/2026-04-18 — WellBuilder incident record (permanent URL)
GET https://notgit.org/report — Submit a GitHub suspension case to the corpus
GET https://notgit.org/replit — Full sovereign deployment cookbook: self-hosted Git + Hetzner CI + CDN edge layer (~$11/mo total)
## The Formula (summary)
1. Deploy self-hosted Forgejo on Railway (~15 minutes, ~$5-10/month)
2. CNAME git.yourproject.org to your Railway deployment
3. Configure Codeberg (nonprofit Forgejo host) as push-mirror
4. Configure GitHub as push-mirror (not primary)
5. Push to your sovereign primary; everything else syncs automatically
## The Five AI-Era Rules
1. README-first publication — no empty repos
2. Cadence discipline — rate-limit automated repo creation
3. PAT scoping — minimum scope for AI assistant tokens
4. Push through sovereign primary — let mirrors propagate
5. Artifact stores for large files — git is not an artifact store
## Key Links
- Federation manifest: https://wellspr.ing/mcp/federation.json
- WellBuilder repos (sovereign): https://git.wellspr.ing/WellBuilder
- Sovereignty notice: https://wellspr.ing/sovereignty
- Sister project: https://noflare.org
- Publishing institution: https://wellspr.ing
NotGit.org is a WellSpr.ing civic project. The formula is given freely: freely given, so freely given.
`;
export async function registerNotgitRoutes(app: Express) {
await ensureNotgitTables();
app.post("/api/notgit/report", async (req: Request, res: Response) => {
try {
const { github_username, suspension_date, ticket_id, work_description, suspected_trigger, resolution_status, contact_email } = req.body;
if (!work_description) return res.status(400).json({ error: "work_description required" });
const ip = (req.headers["x-forwarded-for"] as string || req.ip || "").toString().split(",")[0].trim();
await db.execute(sql`
INSERT INTO notgit_incident_reports
(github_username, suspension_date, ticket_id, work_description, suspected_trigger, resolution_status, contact_email, ip)
VALUES (${github_username || null}, ${suspension_date || null}, ${ticket_id || null}, ${work_description}, ${suspected_trigger || null}, ${resolution_status || 'unknown'}, ${contact_email || null}, ${ip})
`);
console.log(`[NotGit] Incident report received: ${github_username || 'anonymous'}`);
res.json({ ok: true });
} catch (e: any) {
console.error("[NotGit] Report error:", e.message);
res.status(500).json({ error: e.message });
}
});
app.get("/api/notgit/reports", async (req: Request, res: Response) => {
const key = req.headers["x-admin-key"] as string || req.query.admin_key as string;
if (key !== ADMIN_KEY) return res.status(403).json({ error: "Forbidden" });
try {
const rows = await db.execute(sql`SELECT * FROM notgit_incident_reports ORDER BY submitted_at DESC LIMIT 100`);
res.json(rows.rows);
} catch (e: any) { res.status(500).json({ error: e.message }); }
});
// ── Domain-aware page router ─────────────────────────────────────────────
app.use((req: Request, res: Response, next) => {
if (req.method !== "GET" && req.method !== "HEAD") return next();
const hostCandidates = [
req.headers["x-forwarded-host"],
req.headers["x-geo-node-host"],
req.hostname,
req.headers.host,
].flatMap(h => {
if (!h) return [];
const v = (Array.isArray(h) ? h[0] : h) || "";
return v.toString().toLowerCase().split(",").map((s: string) => s.trim().replace(/:\d+$/, "").replace(/^www\./, ""));
});
const isNotgit = hostCandidates.some(h => h.includes("notgit"));
if (!isNotgit) return next();
const p = req.path.replace(/\/$/, "") || "/";
res.setHeader("Content-Type", "text/html; charset=utf-8");
if (p.startsWith("/api/")) return next();
res.setHeader("Cache-Control", "public, max-age=300");
if (p === "/formula") return res.send(FORMULA_HTML);
if (p === "/push-mirror-setup") return res.send(PUSH_MIRROR_HTML);
if (p === "/incident/2026-04-18") return res.send(INCIDENT_HTML);
if (p === "/report") return res.send(REPORT_HTML);
if (p === "/replit") return res.send(REPLIT_HTML);
return res.send(NOTGIT_HOME);
});
console.log("[NotGit] notgit.org routes registered");
}