Der erste Commit eines meiner Projekte heißt "Initial commit" und bringt schon Auth, Datenbank, Zahlungen, i18n und ein Admin-Panel mit. Der zweite heißt oft "remove useless things". Dieses Commit-Paar fasst besser als alles andere zusammen, wie ich arbeite: Ich fange nicht bei null an, ich fange damit an, mein Template zu klonen und zurückzuschneiden.
Zehn Projekte auf diese Art ergeben ein ziemlich geschärftes Urteil darüber, was Kern und was Rauschen ist. Das ist keine Meinung: Es steht in der Historie, und an den Stellen, an denen sich dieselbe Löschung wiederholt.
Was man gewinnt, wenn man so anfängt
Ein SaaS zur Störungsverwaltung für ein Transportunternehmen ging in drei Tagen von null in Produktion. Tag eins: klonen und wegwerfen, was zu viel war. Tag zwei: Schema für Störungen, Bild-Upload, Detail-Drawer, Cache für Listen. Tag drei: Landingpage. Alle drei Tage gingen vollständig in die Domäne. Niemand hat einen Login angefasst.
Ein SaaS zur Bildgenerierung startete genauso. Am Tag des "Initial commit" gab es schon das Schema für generierte Bilder, einen S3-Helper, Branding und Theme. Am nächsten Tag stand bereits die Generierungs-Pipeline. Auth, Postgres mit Drizzle, Stripe und i18n kamen fertig mit.
Was man spart, ist nicht das Schreiben des Logins. Es ist der Monat an kleinen Entscheidungen, den er mitschleppt: wo die Session lebt, wie eine neue Route geschützt wird, was passiert, wenn das Cookie abläuft, wie die Sprachen deklariert werden.
Was immer bleibt
- Auth mit einer einzigen Tür. Keine Funktion
requireSession(), an deren Aufruf jede Seite denken muss, sondern einlayout.tsxim privaten Baum. Eine neue Seite ist dadurch geschützt, wo du sie hinlegst, nicht dadurch, dass du daran denkst, den Check zu kopieren. - Postgres mit einem typisierten ORM und versionierten Migrationen. Hier gibt es ein Detail, das eine Weile braucht, bis man es entdeckt: Wenn die Snapshots von Drizzle aus dem Tritt geraten, diffed
drizzle-kit generategegen eine leere Datenbank und gibtCREATE TABLEfür das gesamte Schema erneut aus. Das behebt man, indem man den initialen Snapshot rekonstruiert, bisgeneratewieder sagt, dass es keine Änderungen gibt. - i18n ab Minute null. Es später einzubauen zwingt dich, jeden String des Projekts anzufassen. Es vorher einzubauen kostet einen Nachmittag.
- Ein minimales Admin. Nutzerliste und vier Metriken. Mehr nicht.
Was immer weggeschnitten wird
Stripe ist der klare Fall. In einer Zeiterfassungs-App wurden die Routen checkout, portal und webhook komplett am selben Tag wie der initiale Commit gelöscht: 338 Zeilen raus. In einem Template für einen internen Viewer genau dasselbe. Zwei verschiedene Projekte, ein Jahr Abstand, dieselbe Löschung.
Die Schlussfolgerung war nicht, Stripe aus dem Template zu entfernen, sondern es hinter ein Flag zu legen:
export const USE_STRIPE = process.env.USE_STRIPE === "true";
export async function POST(request: NextRequest) {
if (!USE_STRIPE) {
return NextResponse.json({ error: "Stripe is not enabled" }, { status: 501 });
}
Ein Projekt ohne Zahlungen setzt jetzt USE_STRIPE=false und macht weiter. Mit einer Feinheit, die zu entdecken Mühe gekostet hat: Der Stripe-Client war null as unknown as Stripe, wenn das Flag aus war, sodass die erste unachtsame Verwendung einen undurchsichtigen TypeError gab, statt zu sagen, was los war. Jetzt ist es ein Proxy, der einen expliziten Fehler wirft.
Der andere Schnitt war RLS. Die Tabellen kamen mit crudPolicy({ role: authenticatedRole, read: authUid(table.userId) }) aus drizzle-orm/neon auf die Welt. Alle wurden entfernt. Der Grund ist nicht, dass RLS schlecht wäre: Die App machte bereits alle Zugriffe über eine Admin-Verbindung vom Server aus, sodass die Policies nie ausgewertet wurden und ein Sicherheitsgefühl vermittelten, das es nicht gab. Zwei Autorisierungsschichten, von denen nur eine lebt, sind schlimmer als eine einzige, weil niemand weiß, welche das Sagen hat.
Und das überdimensionierte Admin. Das Team-Template hat monatelang Admin-Panel und Nutzerverwaltung angesammelt und endete in einem Commit, der alles komplett entfernt. Es vererbt Kosten an alle Kinder und fast keines benutzt es.
Die aufgeschobenen Kosten
Das ist das, was niemand über die Arbeit mit einem Template erzählt: Ein Bug in der Mutter wird bereits geklont in allen Kindern geboren.
Der teuerste, den ich hatte, war eine Zeile im Root-Layout: alternates.canonical: BASE_URL. Jede Seite, die ihr Canonical nicht überschrieb, kanonisierte sich selbst auf die Startseite und flog aus dem Index. Das stand monatelang drin, in jedem Klon. Die Regel, die ich daraus gezogen habe: Ein fehlendes Canonical ist harmlos, ein falsches Canonical ist es nicht. Seitdem setzt das gemeinsame Layout kein Canonical und jede Route setzt ihr eigenes.
Zwei weitere derselben Art:
- Die
.gitignoreignorierte nur.env.local. Der natürliche erste Schritt in einem Klon istcp .env.example .env. Der erste Commit nahm die Schlüssel mit. BASE_URLhartkodiert auf die Domain des Templates. Daran hingenmetadataBase, die Canonicals, das hreflang, die OG-URLs, die Sitemap und die Same-Origin-Validierung des Stripe-Checkouts. Alles in jedem Klon stillschweigend falsch, und ohne einen einzigen Fehler in der Konsole.
Die Spracherkennung war acceptLanguage.includes(locale): Sie ignoriert die Präferenzreihenfolge des Clients und trifft über Substring-Übereinstimmung zu, denn "sl-SI" enthält "sl". Multipliziert mit den Projekten, die diese Zeile geerbt haben.
Wie das Mutter-Template lernt
Die Korrekturen laufen rückwärts: vom Kind zur Mutter. Wenn ein Projekt etwas entdeckt, wandert die Korrektur ins Template und das nächste wird schon mit ihr geboren. Das funktioniert nur, wenn die Korrektur von einem Test begleitet wird, der fehlschlägt, wenn der Fehler wieder eingeführt wird, denn sonst "vereinfacht" das nächste Refactoring die Regel zurück zum Bug.
Die letzte große Härtung meines Templates war ein PR mit 39 Punkten: Idempotenz des Stripe-Webhooks mit einer Tabelle processed_stripe_events, Identität immer aus der Session und nie als Argument in den Server Actions, userId im WHERE jedes Feedback-Lesevorgangs, Same-Origin-Validierung in den Rückkehr-URLs des Checkouts. Minus 11.421 Zeilen, plus 7.268. Fast alles Gelöschte waren Tutorials, die eine Struktur beschrieben, die es nicht mehr gab.
Und das nächste Projekt begann mit chore: inicializa desde la plantilla endurecida. Das ist der ganze Punkt.
Ein Template ist keine Tipparbeitsersparnis, es ist ein Ort, an dem du aufbewahrst, was du schon gelernt hast. Ob es taugt, entscheidet nicht, was es mitbringt, sondern die Disziplin, ihm jede Korrektur zuzuspielen, statt sie in dem Kind zu lassen, in dem sie aufgetaucht ist.
