DevOps-Richtlinien
Die Engineering-Prinzipien, die ich in der Arbeit mit meinen Teams gelernt und verfeinert habe — wie ich Software am liebsten baue, ausliefere und betreibe. Es sind die Praktiken, zu denen ich zuerst greife, nicht die einzigen, mit denen ich arbeiten kann: Jedes Unternehmen ist anders, und ich passe mich dem Team an.
Onboarding
The onboarding journey, in order. Work through the steps top to bottom; the access and links you need are gathered at the bottom.
1. Meet the team
Get introduced to the team: who does what, and who to go to for what. A quick informal round so you know the people before the process.
2. Get a feel of the land
Spend time exploring the team wiki and the repositories at your own pace. Skim the design specs and the DevOps Guidelines. The goal is not to memorize anything, it is to know what exists and where to find it later.
3. Your first ticket
Get set up, then ship a small first ticket end to end to exercise the whole flow.
Set up your machine:
- Install Node using the version pinned in the repo's .nvmrc.
- Clone the repository you will work on.
- Copy .env.example to .env.local and fill in the real values from the team.
- Run npm install.
- Start the app locally with npm run dev; it runs against the local database stack.
- Run npm run check to confirm a green baseline (lint, type-check, build, tests).
Then take a small ticket from the To do column (assigned to you), branch off develop, open a draft PR, self-review, assign reviewers, and get it merged to develop. It then rides the next scheduled release to production. This single pass touches everything: see Tickets, Code Review, Git Flow, and Release Process.
Links and permissions
Request whichever of these you do not already have:
- GitHub: access to the organization and its repositories.
- Atlassian: Jira (the project board) and Confluence (the team wiki).
- Team chat: Slack and Microsoft Teams.
- Provider dashboards relevant to your work: database, auth, error tracking, analytics, and hosting.
- Local secrets: the real values for your .env.local, from the team, never copied out of the wiki.
Everything else is one link away from the onboarding checklist: the wiki space with design specs and process docs, the credentials inventory, the project board, and each repository's README (setup and scripts) and AGENTS.md (coding conventions).
Git Flow
How we organize branches so that scheduled releases stay clean and predictable. This is the branching model the Release Process builds on.
Long-lived branches
Two branches live forever and are never deleted:
- main holds production. Every commit on it corresponds to a release, and a push to main is what triggers a deploy.
- develop is the integration branch for the next scheduled release. Everyday work lands here, and it is kept green at all times.
Short-lived branches
Three kinds of branch are created for a piece of work and deleted once merged:
- feature/* for new work, branched from develop.
- release/* for stabilizing a batch of work before it ships, branched from develop.
- hotfix/* for urgent production fixes that cannot wait, branched from main.
What branches from what
- feature/* — created from develop, merges into develop.
- release/* — created from develop, merges into main and develop.
- hotfix/* — created from main, merges into main and develop.
The rule of thumb: everyday work goes off develop, and only urgent production fixes go off main.
Feature work
Cut feature/* from develop, do the work, and merge it back into develop through a pull request once it passes review and the checks. Keep features small and merge often. The failure mode of this model is a giant feature branch that sits for weeks and produces brutal conflicts at integration time, so prefer branches that live days rather than weeks, behind a feature flag if the work spans a release boundary.
Cutting a release
When the next release is ready, cut release/* from develop for final stabilization (only fixes and release prep, no new features), then merge it into main. That merge is the push that triggers the release. Merge it back into develop too, so any stabilization fixes are not lost. The full readiness and rollback expectations are in the Release Process section.
Hotfixes
When production is broken and cannot wait for the next scheduled release, branch hotfix/* from main, because main is what is live. Fix the problem, merge back into main to trigger an out-of-band release, and merge the same fix into develop so it survives the next scheduled release. Hotfixes are still releases and follow the same readiness and rollback rules.
GitHub Actions
How we use GitHub Actions for continuous integration and release automation. Every push and pull request runs the same checks the team runs locally, and merges to main drive versioning and deploys.
Workflows
Repositories share workflows under .github/workflows/. The ones below cover the full path from a pull request to a released, deployed, announced build. The pr-title, release-please, and patch-notes workflows are identical across repositories. The ci workflow varies per repository to match each stack (database services, provider integrations, and build steps differ); the example below is from the most complete repository.
Continuous integration (ci.yml)
Runs on every push to main and every pull request. It reproduces the full local check in a clean environment against a real Postgres service container, and on deploys it uploads source maps for error tracking.
What it does:
- Spins up a postgres service container and loads the schema from db/init.sql (the service container does not run docker-entrypoint-initdb.d).
- Installs dependencies with npm ci, using the Node version pinned in .nvmrc.
- Runs npm run lint, npx prettier --check ., npx tsc --noEmit, npm test, then npm run build.
- On pushes to main only, injects and uploads source maps to the analytics platform's error tracking with its pinned CLI (skipped automatically if the CLI secrets are not set). The build also uploads source maps to the error monitor when its auth token is present.
These are the same gates described under "What must pass before a release" in the Release Process: CI is the automated layer that blocks a merge unless lint, formatting, type-check, tests, and build pass. Provider secrets come from repository or organization secrets and are never committed.
name: CI
on:
push:
branches: [main]
pull_request:
env:
# Matches docker-compose.yml so DB-backed routes build the same way locally and in CI.
DATABASE_URL: postgresql://app:app@localhost:5432/app_db
# Build-time only; never a real secret in CI.
AUTH_SECRET: ci-not-a-real-secret-ci-not-a-real-secret
jobs:
verify:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app_db
ports:
- '5432:5432'
options: >-
--health-cmd "pg_isready -U app -d app_db"
--health-interval 5s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- name: Initialize database
run: psql "$DATABASE_URL" -f db/init.sql
- run: npm run lint
- run: npx prettier --check .
- run: npx tsc --noEmit
- run: npm test
- name: Build
run: npm run build
env:
CI: true
# Analytics + error-tracking keys and source-map upload tokens
# come from repository secrets here.Pull request title lint (pr-title.yml)
Runs on every pull request and validates that the PR title is a Conventional Commit. Because we squash-merge, the PR title becomes the squashed commit message, which is exactly what release-please reads to compute the next version. This check keeps that input clean. See Versioning for how the prefix maps to a version bump.
name: lint-pr-title
on:
pull_request:
types: [opened, edited, synchronize]
permissions:
pull-requests: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}release-please (release-please.yml)
Runs on every push to main. It maintains a standing release pull request that bumps the version in package.json and updates CHANGELOG.md; merging that PR creates the git tag and the GitHub Release. The version scheme and the full flow live in the Versioning section.
It authenticates with RELEASE_PLEASE_TOKEN, a real personal access token, rather than the default GITHUB_TOKEN. GitHub blocks events caused by the default token from triggering other workflow runs, which otherwise leaves release-please's own PRs stuck and prevents the patch-notes release event below from ever firing.
name: release-please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.jsonPatch notes (patch-notes.yml)
Runs when release-please publishes a GitHub Release (the release event with action published, which fires as the release PR merges). It posts the version's hand-written patch note to Teams and Slack. The note is authored by a human in patch-notes/<version>.md, the same content the app renders, so users and the team see the same curated note. The job checks out the repo, reads the note for the released tag, and skips automatically if no webhook is configured or no hand-written note exists for that version. The auto-generated CHANGELOG.md stays separate as the technical record.
How the triggers map to git flow
- pull_request runs CI and the PR-title lint on feature branches before they merge.
- push to main runs CI, release-please, and (in production) the deploy. A push to main only happens as part of a release, so these run at release time.
- release (published) runs the patch-notes announcement, after release-please cuts the GitHub Release.
Organization settings
release-please needs two organization or repository settings to function:
- Workflow token permission of "read and write" (the release-please workflow also requests contents: write and pull-requests: write explicitly).
- "Allow GitHub Actions to create and approve pull requests" enabled, so release-please can open its release PR.
Versionierung
How we number and track releases, and where the running version is visible to the team and to users.
Versioning scheme
We follow a MAJOR.MINOR.HOTFIX scheme, for example 1.4.2. It resembles semantic versioning, with the third segment scoped to hotfixes rather than general patches. The three segments map to the kinds of release we already run:
- MAJOR is a major feature release, the kind that may ship as a silent release behind planned downtime and roll out gradually.
- MINOR is a regular scheduled release that adds or changes features.
- HOTFIX is an out-of-band fix that branches off main between scheduled releases.
The number only ever goes up. Versions are monotonic: we never reuse or decrement one. Even a rollback moves the version forward, because the revert ships as its own new version.
Single source of truth
The version field in package.json is the single source of truth for the version number: the one artifact every build and every checkout reads. Nobody types the number by hand. release-please computes the next version from the Conventional Commit prefixes since the last release and writes it into package.json inside the release pull request. The only human inputs are the commit prefix (feat, fix, feat!) that decides the size of the bump, and the click that merges the release PR.
Everything else is derived from package.json, not maintained in parallel:
- The git tag and the GitHub Release are created from the package.json version when the release pull request merges. Because nothing types the tag by hand, the tag and the package version cannot drift.
- Build metadata (the commit SHA) is injected at build time for traceability. It identifies which build is running, not which version; the version still comes from package.json.
So package.json is canonical and release-please keeps it current, the git tag is the immutable record derived from it, and the commit SHA ties a running build back to the exact source it came from.
How we track versions and where they show
The current version lives in package.json, the source of truth above. The history of releases is tracked with git tags: each release is stamped as an immutable tag, created from the package.json version when the release pull request merges, so the tags are the permanent record of every version that shipped and let you check out any one of them. release-please creates each tag automatically, alongside a generated CHANGELOG.md and a GitHub Release, and those are the written history the team refers back to. In short: read the current version from package.json; read the history from the tags, changelog, and releases.
In the UI, the running version is shown in two places:
- In settings, displayed as the version plus the commit it was built from (v{version} (commit)), so anyone can check which version they are on and trace it to a build.
- In the patch notes, which are labelled with the version they describe.
Changelog and release notes
We keep two things, for two different readers:
- The changelog (CHANGELOG.md) is generated. release-please builds it from the Conventional Commit messages on each release, one section per version with an entry per commit linking back to its source. It is the technical, commit-level history, mainly for the team.
- The patch notes are written by hand. For each release a human writes a short, plain-language note of what changed, kept in the repository next to the version it describes. These are what users read.
Both are keyed to the version number: each version has one CHANGELOG.md section, one git tag, one GitHub Release, and one hand-written patch note. The hand-written note is what the app shows (in settings and the patch notes view) and what a workflow posts to Teams and Slack when the release is published. The generated changelog stays underneath as the precise technical record.
We hand-write the patch notes on purpose: commit messages are written for engineers and rarely read well to users. A curated note says what actually matters about a release in plain language, while the generated changelog keeps the exact, commit-level trail.
Only releasable commits produce changelog entries: feat, fix, and deps; a chore or build commit does not. To force a specific version when needed, put Release-As: x.y.z in a commit.
Release-Prozess
How a change goes from merged code to running in production, who signs off on it, and how the release is announced.
What triggers a release
A release is triggered when code is pushed to main. Pushing to main runs the release pipeline (build, tag, deploy to production), so main only ever advances as part of a release.
Because we ship on a schedule, code reaches main only at release time. Everyday work lands on develop. When a release is ready, develop is merged into main, and that push starts the release. Hotfixes are the exception: they branch from main, and merging the fix back triggers an out-of-band release.
Deploying and versioning are two separate merges
The push to main and the version number it produces are not the same event. That push deploys the code and also triggers release-please, which opens (or updates) a second, standing pull request that bumps the version, writes CHANGELOG.md, and is titled chore(main): release X.Y.Z. Nothing is versioned, tagged, or announced until that second PR is merged too.
This means a feature can already be live before it has a version:
- The merge to main is what ships the feature. It is running in production the moment that push completes.
- release-please's PR does not gate that. It only formalizes what already shipped into a version number, a CHANGELOG.md entry, and a git tag.
- If the release-please PR is never merged, the feature stays live and unversioned: no clean version number, and no patch-notes announcement in Teams or Slack, since that announcement fires on the git tag the merge creates.
In practice, merge the release-please PR promptly after the feature ships, both to keep package.json and the tag current and because that PR is also the window to add the hand-written patch-notes/{version}.md file before merging, or the announcement finds nothing and silently skips it.
Who can approve and deploy a release
Anyone with trusted access to the GitHub project can approve and deploy a release. Because deploying is a push to main, that access is what gates production.
Non-technical leads may request a release, provided it is ready and well tested. They request it; someone with trusted access carries out the merge to main.
Release readiness
The person or team running a release owns it through to a stable state. There are no fire-and-forget releases. Whoever merges to main must:
- Have ample time set aside, and be prepared for the release before merging.
- Hold active access at merge time to every system the release touches: the GitHub project, the database, hosting, error tracking, and analytics platforms, and any other third-party service relevant to the project.
- Confirm every paid service is green-lit, with no accounting or billing issues that could interrupt it.
- Stay available to triage the release after it ships, watching for errors and regressions and responding to them.
If a release requires or involves a third-party company, confirm their team also has release readiness: that they are prepared, available at merge time, and ready to triage their side before the release goes out.
A release checklist is created and walked through with everyone involved. Before pulling the trigger, every party gives the green light. All parties must be ready to sign off, and the merge to main happens only once they all have.
Before pulling the trigger, take a deep breath and go through the release checklist once more.
What must pass before a release
The same checks run at every stage, so nothing reaches main without clearing them repeatedly. The checks are the project npm scripts:
- npm run test runs the unit and integration suite, and npm run test:coverage runs it with coverage.
- npm run e2e runs the Playwright end-to-end suite against a running build.
- npm run check bundles the full gate: lint, type-check (tsc --noEmit), build, and tests.
These are applied as layers of defense:
- During development, contributors run the checks continuously as they work.
- In code review, reviewers audit the branch and run the same scripts against it.
- In CI, GitHub Actions runs the same checks on every pull request and blocks the merge unless lint, build, tests, and the rest pass.
- On deploy, the hosting platform's build is the final line of defense; a failing build does not go live.
Standards we hold over time
Passing the scripts is the floor, not the whole bar. Reviewers also check that the quality of the codebase keeps rising:
- New code comes with new tests, and those tests are good: meaningful assertions, not coverage padding.
- Coverage does not drop drastically between releases.
- Lint rules are added and tightened over time.
- TypeScript rules are added and tightened over time.
Rollback plan when a release breaks
A full rollback is the last resort, not the first move. It is costly: it can mean reverting third-party services too, and some of those reversions are expensive. So the order is fix first, roll back only if that fails.
Fix forward first: when a release breaks, first try to find and ship a fix for the problem. Only if no fix is in reach do we trigger a full rollback.
Planned downtime for major releases: a major release should include a well-communicated planned downtime for users. Estimate a realistic duration, then double it, and schedule it outside peak usage hours. The application has a downtime mode in which both the frontend and backend are inaccessible; the rollback runs behind that.
How a rollback works: the merge from develop to the release lands on main as a single commit through a pull request, which makes it straightforward to undo. Everyone on the team knows how to revert a commit and how to revert a migration.
Put the application into downtime mode, then:
- If the release required a database migration, revert the migration first.
- Revert the release commit.
- Revert any third-party service changes the release made.
Revert the commit with git revert. The release lands as one squash-merged commit, so:
# Revert the single release commit, then push to main to deploy the revert
git revert <release-commit-sha>
git push origin mainIf the release ever lands as a true merge commit instead of a squash, pick the mainline parent:
git revert -m 1 <merge-commit-sha>Revert the migration by applying its paired down migration before reverting the commit, so the schema and the code move back together: each forward migration is paired with a down script that reverses it.
Communicate and learn: communicate the issue to everyone affected, internally and to users. Afterward, run a post-mortem on what broke and why, and capture the actions that keep it from happening again. Next time we do better.
Hotfixes and emergency releases
When something is broken in production and cannot wait for the next scheduled release, we go outside the normal flow with a hotfix.
A hotfix branches off main instead of develop, because main is what is live. Fix the problem on that branch, then merge it back into main; that push triggers an out-of-band release. Merge the same fix back into develop as well, so it is not lost when the next scheduled release goes out.
Hotfixes are still releases: release readiness and the rollback plan apply unchanged. The person running the hotfix owns it through to a stable state, and we communicate it and run a post-mortem like any other release.
Feature flags: deploying versus releasing
Deploying code and releasing a feature to users are two separate steps, and feature flags are what separate them. A push to main deploys the code; a feature flag decides who actually sees the feature.
For a major feature release, the release begins as a silent release. When it goes live, users do not see the new feature yet: the code is deployed, but the feature is flagged off for them. At first it is enabled only internally, so we can test in production with no user impact.
Once it holds up internally, we roll it out to regular users gradually, rather than flipping it on for everyone at once.
A feature flag is a rollout tool, not a security boundary. Assume a user can trick a flag into evaluating on, so never put a confidential or critical system behind one. Anything that must stay restricted is enforced server-side with real authorization; a flag only decides visibility of work that is already safe for that user to reach.
How we communicate releases
Every release ships with patch notes: a short, plain-language note of what changed for users. Patch notes are written by hand and curated for users, not generated from commit messages. They are published in two places:
- In the application, so users see what changed.
- In Teams and Slack, so the team is informed internally.
The auto-generated changelog is separate. release-please writes CHANGELOG.md and a GitHub Release from the Conventional Commits as the precise, commit-level technical record; the hand-written patch notes are the curated, human-facing version.
Across all of this, under-promise and over-deliver. It is better to commit to less and beat it than to over-commit and fall short. This is why downtime is estimated and then doubled: set expectations you can comfortably exceed.
Dependency-Tracking
How we keep dependencies current and free of known vulnerabilities, using GitHub Dependabot. Dependency updates flow through the same pull-request, review, and release pipeline as any other change.
What Dependabot does
Dependabot has three features, all enabled per repository in GitHub:
- Dependabot alerts flag dependencies with known vulnerabilities, drawn from the GitHub Advisory Database, on the repository's security tab.
- Dependabot security updates open pull requests that bump a vulnerable dependency to a patched version.
- Dependabot version updates open pull requests on a schedule to keep dependencies up to date, even when there is no vulnerability.
Enabling Dependabot also enables the dependency graph, which it needs to see what the repo depends on.
Enabling it
Per repository, under Settings, in the Advanced Security section: enable Dependabot alerts, Dependabot security updates, and Dependabot version updates. Version updates are driven by a committed config file, described below. For an organization-wide default, enable Dependabot in the organization security settings so new repos inherit it.
Configuration
Version updates are configured in .github/dependabot.yml. A baseline config keeps the npm packages and the GitHub Actions current, on a weekly cadence, with non-major updates grouped into a single pull request to reduce noise:
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
open-pull-requests-limit: 10
groups:
minor-and-patch:
update-types: ['minor', 'patch']
commit-message:
prefix: 'chore(deps)'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
commit-message:
prefix: 'ci(deps)'Notes:
- The commit-message.prefix values are Conventional Commits, so Dependabot's pull request titles pass the PR-title lint. Since the repos squash-merge, the PR title is what release-please reads.
- Grouping non-major updates into one pull request keeps review load down; major bumps still come as individual pull requests because they are more likely to break.
- The github-actions ecosystem keeps the action versions in our workflows (for example actions/checkout) patched. Only include it in repos that actually have a .github/workflows/ directory; on a repo with no workflows it has nothing to scan, so leave it out.
- Monorepos need one entry per package directory.
How it fits the release process
A Dependabot pull request is an ordinary pull request. It runs the same CI gate as any change (lint, type-check, build, tests) before it can merge, so a dependency bump that breaks the build or tests is caught automatically. Once merged on a deps or chore(deps) commit, it is a releasable unit, so it appears in the next release that release-please cuts.
Reviewing and merging
- Security updates are the priority. Review the alert on the security tab, open the pull request Dependabot raised, confirm CI is green, and merge.
- Version updates are reviewed like any pull request. The grouped non-major one can usually be merged quickly once CI passes; majors get more scrutiny.
- An alert that is a false positive or does not apply can be dismissed with a reason, which is recorded for auditing.
Code Review
How we review pull requests before they merge: what we expect of a PR, how we keep them reviewable, and who has to approve.
PR size and scope
We keep pull requests small and tightly scoped to their original goal. A PR does one thing; it should not drift out of scope. Boy-scouting (small opportunistic cleanups in the area you are already touching) is encouraged.
Two conventions follow from this:
- Do not fragment one change into a chain of sub-PRs just to keep each one small. Keep the change coherent.
- Stacked PRs are good. For example, a reviewer can open a follow-up PR with suggested changes on top of the author's PR. That is a welcome way to collaborate, not a process violation.
Size is a cultural norm kept reasonable through review, not a hard limit enforced in CI. A PR that has sprawled out of scope gets pushback in review.
Draft and ready for review
Make deliberate use of draft versus ready pull requests. The draft state is part of the workflow, not a formality.
- Open the PR as a draft first, and self-review it before assigning anyone. You usually catch things in your own diff once the change is laid out as a PR.
- Once you assign reviewers, stop actively developing on the branch. From that point the PR should hold still under the reviewers' feet.
- If you find enough that you need to actively develop again, convert the PR back to draft and tell the reviewers clearly.
The reason for the rule: a reviewer should never be bombarded with new changes to pull down in the middle of their review. Draft means "still moving"; ready means "stable, please review."
Who approves before merge
The one strict rule: a PR does not merge until every assigned reviewer has approved. If reviewer A has approved but reviewer B has not, it cannot merge as-is.
Everything around that rule is handled by clear communication rather than rigid policy:
- If a tagged reviewer has not started, or is sitting on the PR, they can be removed from the review.
- Either the PR author or a tagged reviewer can remove reviewers.
- So when A has approved and B has not started, you can remove B and merge, because the assigned set is then just A, and A has approved.
Merging to main for a production release still goes through the Release Process on top of this.
What a reviewer checks
Everything. As a reviewer you are just as responsible for the change as the person who wrote it. That spans correctness and logic, test adequacy, security, readability, and design. A reviewer does not get to wave a problem through and blame the author later; approving a PR means standing behind it.
Review turnaround
Review is the highest-priority task. You review before working on your own tickets. A pull request waiting on you comes first, because unblocking a teammate's finished work matters more than advancing your own unmerged work.
Automated versus human checks
The automated gate is CI: lint, type-check, build, and tests must be green, and Dependabot tracks dependency and security updates. Reviewers do not re-do what CI already covers. Everything CI cannot judge — correctness, design, security reasoning, and readability — is the reviewer's job.
Second reviewer or domain expert
There is no hard rule that forces a second reviewer or a domain expert. You pull one in when the change calls for it, handled by communication like the rest of review, rather than by a required-reviewer policy.
Handling disagreements
A review suggestion is exactly that, a suggestion. It can be dismissed with a counter-argument, but it cannot be silently ignored. If a reviewer's point is brushed aside without a real answer, the cost is concrete: they may withhold their approval, and a PR needs every assigned reviewer's approval to merge.
A disagreement must never become a stalemate. Work toward a decision like this:
- Start in the pull request, but do not grind back and forth asynchronously over text. Once a thread stops converging, take it to a synchronous conversation, in person or over voice. It resolves far faster there.
- Often the author makes the final call once the feedback has been genuinely addressed.
- When it stays contested, bring in third parties: an impartial person, a tech lead, or the relevant domain owner, to weigh in and decide.
- Escalate upstream when needed. Seniority can override any decision; that is the final tie-breaker.
When you disagree and cannot find a solution, the most professional move is sometimes to accept defeat and let the author's code stand. The codebase is not dictated entirely by any one person's opinion, and accepting that is the humble, professional thing to do. It does not mean dropping the concern: after the change merges, raise it again, take ownership of the issue, and bring your fix as its own pull request.
Dokumentation
What we document, where that documentation lives, and how it stays accurate over time.
What we document
The short version: everything that is not obvious from the code itself. In practice that falls into four kinds:
- Product design and architecture decisions, the why behind how the product is built.
- Process and operations: how we branch, release, version, run environments, review code, plan, and track work (these DevOps guidelines).
- How to set up and run each repository.
- The coding conventions the codebase follows.
The authoritative lists are the code and the wiki themselves. The wiki holds the design specs and the process docs, and each repository carries its own setup and conventions, so "what is documented" is whatever is currently in those two places.
Where documentation lives
Three layers, each with a clear job:
- Code comments hold the local why: why a specific piece of code is the way it is. Example: inline comments next to non-obvious code.
- Repo docs (README, AGENTS.md) hold the repo how: how to run it and how to write code in it. Example: README setup and scripts; AGENTS.md conventions.
- The wiki holds product design and team process. Example: the design specs and these DevOps guidelines.
The rule of thumb: if it explains a specific piece of code, it is a comment; if it is specific to one repository, it lives in that repository; if it is product design or how the team works, it lives in the wiki.
Ownership and keeping documentation current
- Ownership follows the change. Whoever changes a thing owns its documentation. There is no dedicated docs owner: the person who alters code, a process, or a decision updates the related docs.
- Updating docs is part of the definition of done. If a change makes a doc wrong, fixing the doc is part of finishing the work, not a separate ticket.
- Docs are living documents. We fix or delete stale documentation when we hit it during related work, the same boy-scout habit we apply to code. There is no scheduled audit; staleness is caught and corrected in the normal flow of work.
Finding documentation
New team members start from a dedicated onboarding doc, a checklist that points them at the right places: the wiki space, the guidelines index for process and DevOps, and each repository's README for setup. The onboarding checklist is the single entry point; from there everything else is one link away.
Planung
The ceremonies we use to plan and coordinate work, how often they run, and how long each should take.
Approach: Kanban, borrowing from Scrum
We do not run Scrum. Work flows continuously, Kanban style: there are no fixed sprints, and the team pulls the next item from a prioritized backlog. We borrow a few Scrum ceremonies because they are useful on their own, without taking on sprints, story points, or the rest of the framework.
Ceremonies and cadence
Three lightweight ceremonies, all tightly time-boxed:
- Daily standup — daily, about 15 minutes. Quick sync on what everyone is working on and surface blockers. Output: shared awareness and a list of blockers to clear.
- Weekly review — weekly, about 30 to 45 minutes. Show progress and what shipped over the week. Output: a clear picture of what moved and what is next.
- Retrospective — monthly, about 45 to 60 minutes. Reflect on how the last month went. Output: a short list of concrete improvements to act on.
The durations are targets, not rules; the point is to keep each ceremony short.
Attendance and facilitation
Everyone is welcome at every ceremony, but attendance is not mandatory: you join the ones relevant to you. There is no heavyweight facilitator role; whoever is running a ceremony keeps it on time.
Keeping ceremonies useful
Two rules keep meetings from turning into overhead:
- Time-box them and end on time.
- Skip a ceremony when there is nothing to discuss. A standup with no updates, or a review with nothing to show, does not need to happen. Meeting for its own sake is exactly what we avoid.
Re-planning when priorities change
Because the flow is Kanban, re-planning is cheap and happens at any time, not only at a fixed planning checkpoint. When priorities shift, re-order the backlog so the new top item is what gets pulled next, and communicate the change clearly so everyone knows what moved and why. There is no sprint commitment to break, so the backlog is simply re-prioritized.
Tickets
How work is captured and tracked as tickets: the states a ticket moves through and the bar it must clear to start and to finish.
Where tickets live
We track tickets in Jira. The link between a ticket and the code is the ticket key referenced in the pull request, so a PR points back to the ticket it implements. Branch names and commits do not need to carry the key; the PR is the link.
Ticket lifecycle
A ticket moves through these board columns:
- Backlog — captured, but not yet prioritized or picked up for work.
- To do — prioritized, assigned to someone, and ready to start (has a clear description).
- Discovery — being investigated before active work: figuring out the approach or scope. Tickets that are already clear skip this and go straight to In progress.
- In progress — being actively worked, the draft-PR stage.
- In review — the PR is open and assigned; CI runs the tests and reviewers check it.
- On staging — merged to develop and deployed to staging, being verified, waiting for the next production release.
- Done — shipped to production in a release.
The columns map to the environments: Discovery and In progress are local development, In review is the PR gate (CI plus reviewers), On staging is the develop/staging environment, and Done is production (main).
Moving between stages
Each move has a clear trigger:
- Backlog to To do: the ticket is prioritized, has a clear description, and is assigned to someone.
- To do to Discovery: only when the approach or scope needs working out first.
- To do to In progress: when the ticket is already clear, it skips Discovery and goes straight to In progress.
- Discovery to In progress: the approach is decided, so coding starts.
- In progress to In review: the work is ready and the PR is opened and assigned (it leaves draft).
- In review to On staging: reviewers approve, CI is green, and the PR merges to develop.
- On staging to Done: the next scheduled release deploys develop to production. This move is automatic; the release carries the ticket.
Skips and loops:
- Discovery is the one optional stage. A clear ticket skips it; a fuzzy one passes through it first.
- When review turns up changes, the ticket stays In review while the author addresses them. It does not drop back to In progress; the back-and-forth happens within review.
- A ticket can become blocked at any stage. Flag it and raise it at standup; it returns to its stage once unblocked.
Definition of ready
A ticket is ready to start when it has a clear description: what needs doing and why. To move from Backlog to To do it also needs to be prioritized and assigned to someone.
There is no separate refinement process. A clear title and description is enough to pick a ticket up. Extra detail such as acceptance criteria or a task breakdown is optional, added only when it genuinely helps, never as a gate.
Definition of done
The assignee's job is done when the change is merged to develop. Merging to develop already requires an approved review and green CI, so "merged to develop" carries those with it.
From there the ticket sits On staging and moves to Done automatically when the next scheduled release deploys develop to production. That final step is the release carrying the ticket, not extra work on the ticket itself.
Ticket types
Four types, differing by intent:
- Story: a unit of user-facing functionality.
- Bug: a defect to fix.
- Task: technical work or a chore with no direct user-facing feature.
- Spike: a time-boxed investigation or discovery. A spike ticket spends most of its life in the Discovery column.
Creating tickets
Anyone on the team can create a ticket, and creating one should take under a minute. The minimum is a clear title and a description of what and why. There is no refinement step to clear, so keep tickets small and create them freely.
Ticket templates
Templates are deliberately tiny. A title plus a sentence is usually enough; fill only the lines that help. Making a ticket should never be a chore.
- Story: title (what the user can do); what and why in one or two sentences.
- Bug: title (what is broken); what happens, and what should happen instead; steps to reproduce, if known.
- Task: title (the technical work); what and why in one or two sentences.
- Spike: title (the question to answer); goal (what we want to learn or decide); time-box (how long before we stop and report back).
Estimating, prioritising, and the backlog
We do not estimate tickets; there are no story points. Priority is simply the order of the backlog, Kanban style. The team self-organizes: there is no single backlog owner. The team collectively keeps the backlog ordered and pulls the next item from the top. Re-prioritising is just re-ordering the backlog.
Blocked or stalled tickets
When a ticket is blocked, flag it as blocked and note what is blocking it, then raise it at the daily standup so it gets unblocked or escalated. The point is that a stalled ticket stays visible and gets talked about, rather than sitting silently.
Umgebungen
The environments we run (local, staging, production), how faithfully each reproduces production, and how a change is promoted between them.
Environments we maintain
Three environments, each mapped to a branch and its own database:
- Local — any working branch, local database stack, telemetry off. For local development.
- Staging — the develop branch, its own staging database project, development telemetry. The shared environment for the whole team.
- Production — the main branch, the production database project, production telemetry. For live users.
Each database is its own airgapped system. Databases are never shared or cloned between environments, so no environment runs on another's data.
How providers model environments
The two main providers split environments differently, which is why telemetry and database are handled separately above:
- The analytics platform supports staging and production inside one project, using its environments feature. So the development and production telemetry split lives within a single project, not two separate projects.
- The database platform supports two models. Today we run a separate project per environment: the staging and production projects are distinct, which is what keeps the databases airgapped. It also supports keeping staging and production inside a single project through branching, so this is a choice rather than a constraint: production as the project's main branch, with a persistent branch (long-lived, never auto-paused, unlike per-pull-request preview branches) serving as staging. Each branch is an isolated copy of the project's schema, functions, and configuration, without the production data, and changes are promoted by merging the staging branch into production.
How closely local mirrors production
Local runs the app against the local database stack, so the schema and data plane behave like production. It diverges from production in two deliberate ways:
- Third-party providers do not run locally. Analytics and error tracking are off in local development. Those providers run in two environments, development for staging and production for production, never against local.
- The local database is its own airgapped system, independent of staging and production, never a copy of either.
So local mirrors production's code and schema closely, but not its telemetry, and never its data.
Promotion path from staging to production
Each environment maps to a branch, and a change is promoted by moving up that ladder: local (any working branch) to staging (develop) to production (main).
- A feature branch merges into develop, which deploys to staging, where the whole team uses it.
- When a release is ready, develop merges into main, which deploys to production.
Configuration and secrets
Secrets are kept on the platform, not in the codebase. Local values live in gitignored .env.local files. The full inventory of credentials, and where each lives, is in the Credentials section.
Test data
Test data is seeded manually or through scripts. No environment uses a copy of production data: because the databases are airgapped and never shared or cloned, each environment seeds its own data independently.
Access and rebuilding
Production is gated by trusted access to the GitHub project, since deploying to it is a push to main. Each environment rebuilds from its branch, its own database project, and the seed scripts, so a rebuild does not depend on any other environment.
Domains
Where the app lives on the public internet: the hosting platform, the domain it is served from, the subdomains it needs, and how each environment maps to a URL.
Hosting
The app is hosted on a platform that builds and serves it and manages TLS certificates automatically for any domain attached to the project. A push to main deploys production and a push to develop deploys staging.
Because the host terminates TLS and issues certificates per attached domain, choosing a domain is mostly a matter of registering it, pointing DNS at the host, and attaching it to the project. There is no separate certificate or load-balancer setup to manage.
Production domain
Choosing the production root domain is a significant decision because it is hard to change once users, links, search indexing, OAuth redirect URLs, and email all depend on it, so it is worth getting right before launch rather than after.
What to settle when the domain is chosen:
- The apex name and TLD (for example a dedicated apex, or a subdomain under an existing company domain).
- Whether the app is served from the apex or from a subdomain such as app, with the apex reserved for a marketing or landing page.
- The registrar and where DNS is managed.
Subdomains
Candidate subdomains worth considering when the domain is chosen:
- www — redirect to the apex, or the reverse, so both forms resolve.
- app — the application itself, leaving the apex for a marketing or landing page.
- api — the backend or provider API, if it is ever served on its own host rather than under the app's route handlers.
- docs — public documentation, if any is published outside the wiki.
Domains per environment
Each environment maps to a branch and a deploy:
- Local — any working branch, at localhost.
- Staging — develop, on the host's auto-generated preview URLs until a stable staging subdomain is decided.
- Production — main, on the chosen production domain.
Knock-on effects of the domain choice
The domain is not just a DNS record; several systems are configured against it and need updating when it is set:
- Authentication: the auth provider's allowed origins and sign-in, sign-up, and redirect URLs are environment-specific and must list the real domain.
- Database platform: allowed redirect URLs and CORS origins for the production domain.
- Telemetry: any allowed-origin or CORS configuration in analytics and error tracking that references the site URL.
- Hosting: attaching the domain to the project and setting the production and preview environment URLs.
Credentials
A living inventory of the credentials the app needs, what each is for, and where it lives.
This inventory lists credential names only, never values. Real values live only in gitignored .env.local files, GitHub Actions or organization secrets, and the host's environment variables. Never paste a secret value into the wiki. When a credential is added or rotated, update the inventory with its name and purpose, not its value.
Where credentials live
- .env.example (committed) lists the variable names a repo needs, with placeholder values.
- .env.local (gitignored) holds real local values and is never committed.
- GitHub Actions secrets hold what CI needs at build time.
- The host's environment variables hold what production needs at build and runtime.
- Provider keys originate in each provider's dashboard.
"Public" means the variable is intentionally shipped to the browser; it is not a secret but is environment-specific. "Secret" must never be exposed client-side.
Database platform
Postgres database, authentication backing, file storage, and the management API:
- Project API URL (config).
- Publishable (anon) key (public): row-level-security-scoped, useful only with a user JWT.
- Service-role key (secret): full-access admin key that bypasses row-level security. Used only out of band (admin, migrations), never in app code.
- Management API access token (secret): for schema migrations.
- Project reference id (config).
Authentication provider
- Frontend publishable key (public).
- Backend API key (secret).
- Sign-in route, sign-up route, and post sign-in/sign-up redirect URLs (config).
Analytics platform
- Project API key for the client (public).
- Ingestion host (config).
- CLI token for source-map upload in CI (secret).
- Environment id for the CLI (config).
Error monitoring
- DSN for the client (public).
- Source-map upload token for build/CI (secret).
- Organization and project slugs (config).
Chat webhooks (patch notes)
- Slack incoming webhook for patch-note announcements in CI (secret).
- Teams incoming webhook for patch-note announcements in CI (secret).
GitHub Actions secrets
The values CI needs at build time, configured as repository or organization secrets:
- Build and source maps: the analytics and error-monitoring keys and tokens above.
- Patch notes: the Slack and Teams webhook URLs.
- release-please: a real personal access token, not the default GITHUB_TOKEN. GitHub blocks events caused by the default token from triggering other workflows, so release-please's own PRs and the patch-notes announcement never fire without a real PAT here.
- The PR-title lint uses the built-in GITHUB_TOKEN only, no stored secret.
Drittanbieter-Dienste & Bibliotheken
The services and libraries the production app is built on: the final intended stack, decided across proving-ground demos, not a dump of every dependency in those demos.
Decisions
- Authentication: Clerk, chosen after evaluating it head-to-head against WorkOS AuthKit over the same database schema.
- Persistence: Supabase (Postgres, auth backing, storage), rather than direct-pg or standalone-API setups used as scaffolding in demos.
- Internationalisation: i18next.
- Telemetry: Sentry and PostHog.
- Testing: Vitest and Playwright.
Hosted services
- Supabase — Postgres database, authentication backing, file storage, management API.
- Clerk — authentication.
- Sentry — error and performance monitoring.
- PostHog — product analytics and error tracking.
- Vercel — hosting and deploys.
- GitHub Actions — continuous integration and release automation.
- Slack — patch-note announcements via incoming webhook.
- Microsoft Teams — patch-note announcements via incoming webhook.
- Atlassian Jira and Confluence — planning and knowledge base.
Libraries
Third-party npm packages in the final stack, grouped by role:
- Framework and runtime: next, react, react-dom.
- Styling and UI: tailwindcss, @tailwindcss/postcss, tw-animate-css, class-variance-authority, clsx, tailwind-merge, lucide-react, shadcn, @base-ui/react.
- State, data, forms, validation: zustand, @tanstack/react-query, react-hook-form, zod.
- Relationship graph and visualisation: @xyflow/react (reactflow), three, @react-three/fiber, @react-three/drei, topojson-client, world-atlas.
- Authentication: @clerk/nextjs.
- Database and storage: @supabase/supabase-js, @supabase/ssr, supabase (CLI).
- Internationalisation: i18next.
- Telemetry: @sentry/nextjs, posthog-js, posthog-node.
- Testing: vitest, @vitest/coverage-v8, @playwright/test, @testing-library/react, @testing-library/dom, @testing-library/jest-dom, jsdom.
- Tooling (dev): typescript, eslint, eslint-config-next, eslint-config-prettier, eslint-plugin-prettier, eslint-plugin-tailwindcss, prettier, postcss, autoprefixer.
The choice policy behind all of this: adopt well-maintained, widely used options with compatible licenses, validate each in a focused demo before committing it to the final stack, and record what each service is used for and who owns the account.