- This is not a grid of CI vendors. GitLab CI, Jenkins, CircleCI, Buildkite,
- and Azure DevOps are not adapters here.
-
-
-
-
- GitHub App
- Required. Webhooks, installation tokens, Check Runs.
-
-
- Coolify
- Optional API: inventory, repo picker, install-worker. Not a job runner.
-
-
- Docker
- runtime: and fork jobs via docker run.
-
-
-
Process executor
-
Default when runtime is empty. Steps run in the worker.
+
+
+ Start with the App
+ Quickstart
+
+
+
+
+
+ {
+ surfaces.map((surface) => {
+ const Tag = surface.href ? 'a' : 'div';
+ return (
+
+
+ {surface.status}
+
+ {surface.title}
+
+ {surface.cta && (
+
+ {surface.cta}
+ →
+
+ )}
+
+ );
+ })
+ }
+
+
+
+
+ the trust boundary worth understanding before you tick a lot of them.',
+ },
+ {
+ title: 'Leave the executor alone',
+ detail:
+ 'With no runtime:, steps run in the worker process. Most installs never need anything past this point.',
+ },
+ {
+ title: 'Add Docker only if you need it',
+ detail:
+ 'Set CI_DOCKER_HOST — or mount a socket — when you want container runtimes or fork pull requests.',
+ },
+ ]}
+ />
+
+
+
+
+
+
+ User tokens and OAuth tokens are refused by GitHub for this — the
+ Check Runs API is App-only. That single fact is why the integration
+ list starts and ends where it does.
+
+
+ A GitHub App also has exactly one webhook URL, which
+ is why Coolify’s own GitHub connector cannot stand in: its webhook
+ already belongs to Coolify’s deploy pipeline, and its manifest carries
+ no checks permission.
+
+
+
+
+ Receives check-suite webhooks, mints a short-lived installation token
+ per job, and writes the Check Run back onto the commit.
+
+
+ Get created for you, act on repos you have not bound, or hand its
+ private key to a job.
+
+
-
+
+ ADR 003
+ Set one up
+
+
+
+
+ runtime:, and for every fork pull request. The engine comes from CI_DOCKER_HOST, else DOCKER_HOST, else the default socket — and it can be a different machine, over Docker’s own remote API.',
+ href: `${DOCS}/adr/004-docker-executor/`,
+ linkLabel: 'ADR 004',
+ },
+ {
+ question: 'What happens if the engine is unreachable?',
+ answer:
+ 'The job fails rather than quietly falling back to running your commands as a process on the host. The service itself boots and reports checks fine without an engine — only runtime: jobs and fork PRs need it.',
+ },
+ {
+ question: 'Do job containers get the Docker socket?',
+ answer:
+ 'No — a job never gets a handle on the engine that started it. What a container is and is not given is on security.',
+ },
+ {
+ question: 'What is Coolify used for?',
+ answer:
+ 'Server inventory, a repository picker for the bindings screen, and an install-worker call that creates the compose application for you. That is the whole surface.',
+ href: `${DOCS}/setup/coolify/`,
+ linkLabel: 'Coolify',
+ },
+ {
+ question: 'Can Coolify run the jobs?',
+ answer:
+ 'No. A Coolify token cannot start a docker run. To put jobs on another machine you point CI_DOCKER_HOST at that machine’s Docker engine — that is Docker’s remote API, not Coolify’s.',
+ href: `${DOCS}/understanding/deployment/`,
+ linkLabel: 'Deployment',
+ },
+ ]}
+ />
+
+
+
+
+
+
+ “Portable” here means the same .ci.yml and the same worker
+ on your own host — not one config that targets every CI vendor. See{' '}
+ the product page for what that does and does not buy you.
+
+
+
-
diff --git a/src/pages/open-source.astro b/src/pages/open-source.astro
index 490c779..4cb2440 100644
--- a/src/pages/open-source.astro
+++ b/src/pages/open-source.astro
@@ -1,54 +1,236 @@
---
import MarketingPage from '../layouts/MarketingPage.astro';
-import { CTA, DOCS, DOCS_REPO, REPO, WEBSITE_REPO, RELEASE } from '../lib/site';
+import PageHero from '../components/marketing/PageHero.astro';
+import Section from '../components/marketing/Section.astro';
+import Grid from '../components/marketing/Grid.astro';
+import Card from '../components/marketing/Card.astro';
+import Actions from '../components/marketing/Actions.astro';
+import CtaLink from '../components/marketing/CtaLink.astro';
+import Callout from '../components/marketing/Callout.astro';
+import CodePanel from '../components/marketing/CodePanel.astro';
+import Prose from '../components/marketing/Prose.astro';
+import Faq from '../components/marketing/Faq.astro';
+import { DOCS, DOCS_REPO, REPO, WEBSITE_REPO, RELEASE } from '../lib/site';
+
+const repos = [
+ {
+ name: 'openpreflight/openpreflight',
+ href: REPO,
+ detail: 'The Go binary — API, UI, webhook receiver, runner.',
+ licence: 'Apache-2.0',
+ },
+ {
+ name: 'openpreflight/docs',
+ href: DOCS_REPO,
+ detail: 'Reference documentation at docs.openpreflight.xyz.',
+ licence: 'MIT',
+ },
+ {
+ name: 'openpreflight/website',
+ href: WEBSITE_REPO,
+ detail: 'This site, plus the shared brand images.',
+ licence: 'MIT',
+ },
+ {
+ name: 'openpreflight/.github',
+ href: 'https://github.com/openpreflight/.github',
+ detail: 'The organisation profile.',
+ licence: '—',
+ },
+];
---
- Open source
- Inspectable worker, public repos
-
- The worker holds GitHub App PEMs. Open source is how you can read what it
- does with them. License is Apache-2.0 on the binary; the two sites are MIT.
-
-
- Repositories
-
-
- Contribute
-
- Issues and pull requests on the code repo. Development loop:
- development.
- Website and docs have their own CONTRIBUTING files. There is no foundation,
- board, or RFC process to join.
-
-
- Releases
-
- v1.0.0 is tagged (29 August 2026). The changelog lives in
- the code repo. The GitHub Release has linux amd64 and
- arm64 binaries. Pin the published image with{" "}
- OPENPREFLIGHT_VERSION=1.0.0.
-
-
- Security reports
-
- SECURITY.md
- — not the public issue tracker for vulnerabilities.
-
-
-
+
+
+ View source
+ Apache-2.0
+
+
+
+
+
+
+
+
+
+ The whole implementation lives under internal/. There is
+ no pkg/, nothing importable, and no extension API — which
+ means there is also no plugin boundary where behaviour can hide.
+
+
+ go test ./... needs no network and no credentials: the
+ GitHub and Coolify APIs are faked, and the clone and pipeline tests run
+ against a real git-http-backend server over a fixture
+ repository.
+
+
+ That is the practical reason to open source a worker that holds App
+ private keys — a reviewer can read every path a key takes in an
+ afternoon, and run the suite without asking anyone for access.
+
+
+
+
+
+
+
+
+
+ Bug fixes land fastest when they arrive with a test that fails without
+ the patch. go test ./... runs offline — GitHub and Coolify
+ are faked.
+
+
+ Small things the scope already covers but the code does not do yet. Not
+ features from the Not-in-v1 list.
+
+
+ Corrections where the documentation drifted from behaviour. The website
+ and docs repos have their own CONTRIBUTING files.
+
+
+
+ There is no foundation, board, or RFC process to join. Issues and pull
+ requests on the code repository are the whole process.
+
+
+ CONTRIBUTING.md
+ Development loop
+ Open an issue
+
+
+
+
+
+
+ v1.0.0 is tagged (29 August 2026). The changelog lives
+ in the code repo. The GitHub Release has
+ linux amd64 and arm64 binaries. Pin the published image with{' '}
+ OPENPREFLIGHT_VERSION=1.0.0.
+
+
+ Vulnerabilities go through SECURITY.md,
+ never the public issue tracker.
+
+
+
+
+ A CI worker with your App’s private key is a high-trust component. The{' '}
+ security page lists what it is allowed to do; the
+ source is how you check that the list is true.
+
+
+
+
+
+
+
+
+ Apache-2.0
+
+
The binary
+
+ A permissive licence with an explicit patent grant, for the thing you
+ will actually run on your own infrastructure. Fork it, run it
+ internally, or ship a modified build.
+
+
+ Read the licence →
+
+
+
+
+ MIT
+
+
The two sites
+
+ The documentation and this marketing site. Lighter terms for prose and
+ markup that nobody deploys as infrastructure — take a page as a
+ starting point if it is useful.
+
+
+
+
+
+
+
diff --git a/src/pages/pipeline.astro b/src/pages/pipeline.astro
index cb93c5a..6a92ab0 100644
--- a/src/pages/pipeline.astro
+++ b/src/pages/pipeline.astro
@@ -1,58 +1,268 @@
---
import MarketingPage from '../layouts/MarketingPage.astro';
+import PageHero from '../components/marketing/PageHero.astro';
+import Section from '../components/marketing/Section.astro';
+import Grid from '../components/marketing/Grid.astro';
+import Card from '../components/marketing/Card.astro';
+import CodePanel from '../components/marketing/CodePanel.astro';
+import Actions from '../components/marketing/Actions.astro';
+import CtaLink from '../components/marketing/CtaLink.astro';
+import Callout from '../components/marketing/Callout.astro';
+import Faq from '../components/marketing/Faq.astro';
+import MarkList from '../components/marketing/MarkList.astro';
+import Prose from '../components/marketing/Prose.astro';
import { CTA, DOCS, REPO } from '../lib/site';
+
+const fields = [
+ {
+ name: 'runtime',
+ required: 'optional',
+ detail:
+ 'Container image for the steps. Omit it and they run in the worker process.',
+ },
+ {
+ name: 'install',
+ required: 'optional',
+ detail: 'Shell command. Usually the lockfile install for your language.',
+ },
+ { name: 'test', required: 'optional', detail: 'Shell command. Its exit code gates the commit.' },
+ { name: 'build', required: 'optional', detail: 'Shell command, run after test.' },
+ {
+ name: 'timeout',
+ required: 'optional',
+ detail: 'How long the run may take before it is cut off.',
+ },
+];
+
+const cannot = [
+ 'Stages or
needs:',
+ 'Fan-out and parallel steps',
+ 'Conditional steps',
+ 'Caches between runs',
+ 'Artifacts between steps',
+ 'Build matrices',
+ 'Path filters for monorepos',
+ 'Steps that are not install / test / build',
+];
+
+const overrides = [
+ 'the branch list',
+ 'the check name',
+ 'the pipeline file path',
+ 'the timeout',
+ 'the install / test / build commands',
+ 'whether logs are shareable',
+];
+
+const resolution = [
+ {
+ order: '01',
+ title: 'The repo’s pipeline file',
+ detail: '
.ci.yml at the root of the commit being checked.',
+ },
+ {
+ order: '02',
+ title: 'The binding’s command overrides',
+ detail: 'Set in the UI, for repos you do not want to add a file to.',
+ },
+ {
+ order: '03',
+ title: 'Node defaults from
package.json',
+ detail:
+ 'Lockfile install, then
test and
build only if those scripts exist.',
+ },
+ {
+ order: '04',
+ title: 'Nothing to run',
+ detail: 'The check reports
skipped — not failed.',
+ },
+];
---
- Pipeline
- What runs before the Check Run completes
-
- There is no check registry. Steps are shell commands in
- .ci.yml — illustrations like go test or
- npm test are commands you author, not first-class products.
-
-
- The file
- {`runtime: node:24
+
+
+ Pipeline docs
+
+ Example file
+
+
+
-
- Sample in the code repo:
- examples/.ci.yml.
- Default filename is .ci.yml, not openpreflight.yaml.
-
-
- Resolution order
- Highest first:
-
- - the repo’s pipeline file
- - the binding’s command overrides
- -
- Node defaults from
package.json (lockfile install, then
- test / build only if those scripts exist)
-
- - nothing to run → the check is skipped, not failed
-
- A failing step stops the run; later steps are reported skipped.
-
- Runtime
-
- Omit runtime to run in-process. A non-empty image uses
- docker run --rm. If the engine is unreachable, the job fails
- instead of falling back. Fork jobs always use Docker.
-
-
- The contract, not this page, is source of truth:
- pipelines.
-
-
-
+timeout: 15m`}
+ />
+
+
+
+
+ {
+ fields.map((field) => (
+
+
-
+
{field.name}
+
+ {field.required}
+
+
+ -
+
+
+
+ ))
+ }
+
+
+
+ A file that only sets runtime: or timeout:
+ still applies those, while the commands come from the binding or from
+ package.json. Image names are allow-listed — no shell
+ metacharacters and no leading -.
+
+
+
+
+
+
+ {
+ resolution.map((step) => (
+ -
+ {step.order}
+
+
+
+ ))
+ }
+
+
+ A failing step stops the run; later steps are reported skipped. Nothing to
+ run at all is a skipped check, which is a different signal from a failure.
+
+
+
+
+
+
+
Node defaults
+
+
+ With no pipeline file and no overrides, a Node repo is inferred from
+ package.json: the install command follows the lockfile
+ — npm ci, pnpm, or yarn — and
+ test and build run only if those
+ scripts exist.
+
+
+ That is the whole inference. No other language is guessed, and
+ nothing is invented for a repo that has neither a file nor scripts —
+ it reports skipped.
+
+
+
+
+
Binding overrides
+
+ For a repo you would rather not add a file to, a binding can override:
+
+
+
+ At run time precedence is binding → App → settings.
+
+
+
+
+
+
+
+
+ Omit runtime and steps run as local processes on the host
+ that runs the binary. Nothing else to install.
+
+
+ A non-empty image uses docker run --rm. If the engine is
+ unreachable the job fails instead of falling back. Fork jobs always use
+ Docker.
+
+
+
+
+ This page is positioning. The contract — full field list, precedence
+ rules, and what each executor guarantees — is{' '}
+ the pipeline reference.
+
+
+
+
+
+
+
+ Every job is a fresh shallow clone, so nothing is carried between runs or
+ handed to a later step. If your build needs any of the above, you want a
+ pipeline engine rather than this —{' '}
+ the comparison{' '}
+ names the candidates.
+
+
+
+
+ skipped, not failed. That is a different signal on purpose — a repo with no pipeline is not a broken build.',
+ },
+ {
+ question: 'How long can a run take?',
+ answer:
+ 'Until the timeout. timeout in the file overrides the binding, which overrides the settings default. A job that exceeds it is cut off and reported.',
+ href: `${DOCS}/start/configuration/`,
+ linkLabel: 'Configuration',
+ },
+ {
+ question: 'How big can a log get?',
+ answer:
+ 'There is a byte cap; the log stops growing at it while the run continues. Logs and their job rows are pruned on a retention window, so history is finite by design.',
+ href: `${DOCS}/using/logs/`,
+ linkLabel: 'Logs',
+ },
+ ]}
+ />
+
+
diff --git a/src/pages/product/index.astro b/src/pages/product/index.astro
index dcecda0..09df711 100644
--- a/src/pages/product/index.astro
+++ b/src/pages/product/index.astro
@@ -1,89 +1,335 @@
---
import MarketingPage from '../../layouts/MarketingPage.astro';
+import PageHero from '../../components/marketing/PageHero.astro';
+import Section from '../../components/marketing/Section.astro';
+import Grid from '../../components/marketing/Grid.astro';
+import Card from '../../components/marketing/Card.astro';
+import Steps from '../../components/marketing/Steps.astro';
+import MarkList from '../../components/marketing/MarkList.astro';
+import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro';
+import CodePanel from '../../components/marketing/CodePanel.astro';
+import Actions from '../../components/marketing/Actions.astro';
+import CtaLink from '../../components/marketing/CtaLink.astro';
+import Prose from '../../components/marketing/Prose.astro';
+import Callout from '../../components/marketing/Callout.astro';
+import Faq from '../../components/marketing/Faq.astro';
import { CTA, DOCS, REPO, RELEASE } from '../../lib/site';
+
+const panelSteps = [
+ { name: 'install', command: 'npm ci', duration: '8s', width: '19%' },
+ { name: 'test', command: 'npm test', duration: '21s', width: '50%' },
+ { name: 'build', command: 'npm run build', duration: '13s', width: '31%' },
+];
+
+const fit = [
+ 'You want CI on private GitHub repositories',
+ 'You do not want GitHub Actions runners',
+ 'You do not want to learn a pipeline DSL',
+ 'You already have a machine to run it on',
+];
+
+const lookElsewhere = [
+ 'Your build is more than three commands',
+ 'You need more than one machine',
+ 'Your workflows are already Actions YAML',
+ 'More than one person configures CI',
+];
+
+const runSteps = [
+ {
+ title: 'GitHub POSTs the webhook',
+ detail:
+ 'The check suite event arrives at
/webhook/{slug}. The API validates it, confirms the binding is enabled, and enqueues.',
+ },
+ {
+ title: 'The runner takes the job',
+ detail:
+ 'The worker checks out the immutable SHA into a fresh workspace, strips the remote, and reads the pipeline.',
+ },
+ {
+ title: 'install / test / build',
+ detail:
+ 'Steps run through the executor — the worker process by default, or
docker run when
runtime: is set.',
+ },
+ {
+ title: 'One Check Run comes back',
+ detail:
+ 'Conclusion plus a log tail land on the commit. The details URL points at your instance, where the full log lives.',
+ },
+];
+
+const dedup = [
+ {
+ title: 'GitHub asks twice',
+ detail:
+ 'A second
requested delivery for a commit already in flight is answered
already queued. Delivery ids still in flight are deduped.',
+ },
+ {
+ title: 'A human presses Re-run',
+ detail:
+ 'A
rerequested delivery supersedes the run in flight — the old one is cancelled first, then the new one is queued.',
+ },
+ {
+ title: 'A newer commit lands',
+ detail:
+ 'An older in-flight job for the same repo and ref on a
different SHA is cancelled, and the new SHA is enqueued in its place.',
+ },
+];
+
+const surfaces = [
+ {
+ title: 'The web UI',
+ body: 'Server-rendered pages for Apps, bindings, settings, jobs, and log pages. This is where a first install gets configured.',
+ },
+ {
+ title: 'The JSON API',
+ body: 'The same handlers answer JSON.
POST /api/v1/login returns a bearer token; jobs can be listed, re-run, and cancelled from a script.',
+ },
+ {
+ title: 'The webhook',
+ body: '
POST /webhook/{slug} is the only route GitHub touches, and it is HMAC-verified against that App\u2019s secret.',
+ },
+ {
+ title: 'A health probe',
+ body: '
GET /health is liveness. It answers 503 when the process cannot read SQLite, which is what a reverse proxy should watch.',
+ },
+];
+
+const inbound = [
+ { label: 'GitHub', detail: 'POST /webhook/{slug}' },
+ { label: 'Browser / CLI', detail: 'session or Bearer' },
+];
+
+const core = [
+ { label: 'api', detail: 'validate · enqueue · serve UI' },
+ { label: 'queue.Runner', detail: 'one live run per commit' },
+ { label: 'SQLite', detail: 'apps · bindings · jobs · secrets' },
+];
+
+const outbound = [
+ { label: 'GitHub App', detail: 'creates the Check Run' },
+ { label: 'workspace', detail: 'checkout of the exact SHA' },
+ { label: 'pipeline', detail: 'reads .ci.yml' },
+ { label: 'executor', detail: 'process or docker run' },
+];
---
- Product
- Self-hosted Check Runs for private repos
-
- One Go binary and one SQLite file. You register a GitHub App, bind repos in
- the UI, and every commit gets a Check Run with logs that live on your server.
-
-
- v1.0.0 is the tagged v1 release.{" "}
- GitHub Release ·{" "}
- Changelog.
-
-
- Fit
-
- You want CI on private GitHub repositories without GitHub Actions runners
- and without a pipeline DSL. You already have a machine. The worker is the
- product, not a mode.
-
-
- Full platforms, hosted control planes, and Kubernetes-oriented runners
- already fill a different slot. This one does not replace them. See
- why it is this shape and
- the FAQ.
-
-
- What it is not
- v1 does not include GitHub Actions YAML, actions/runner, matrices, caches, artifacts, or creating GitHub Apps for you. Those stay on Not in v1.
-
- How a run happens
- The same loop as the docs, in marketing nouns:
- {`GitHub ──POST /webhook/{slug}──► api ──enqueue──► queue.Runner
- │ │
-Browser / CLI ──session/Bearer──► api │ ├── GitHub App (Check Run)
- │ ├── workspace (exact SHA)
- └── SQLite ├── pipeline (.ci.yml)
- └── executor (process or docker run)`}
-
- Webhook → queue → checkout of the immutable SHA → install/test/build → one
- Check Run. One live run per commit. Details:
- architecture
- and
- ADR 005.
-
-
- Portable in the real sense
-
- The same .ci.yml and the same worker run on your host.
- That is not “any CI vendor.” There are no GitLab, Jenkins, or CircleCI
- adapters.
-
-
- Configuration
-
- Bindings and Apps are rows in SQLite, edited in the web UI. Optional
- .ci.yml in the repo supplies commands; binding overrides and
- package.json scripts fill gaps. Resolution order lives in
- pipelines. Marketing summary:
- /pipeline.
-
-
- Results
-
- GitHub shows the Check Run. The details URL is GET /runs/{id}
- on your instance — session by default, or a shareable log if that binding
- opted in.
-
-
- Security
-
- Claims match the docs only:
- security model.
- Marketing page: /security.
-
-
-
+
+
+ Quickstart
+ See a live run
+
+
+
+
+
+
+
+ Tagged 29 August 2026.{' '}
+ GitHub Release
+ {' · '}
+ Changelog.
+
+
+
+
+
Use it when
+
+
+ Three concrete versions of that are written up as{' '}
+ use cases.
+
+
+
+
Look elsewhere when
+
+
+ Each of those is a real limit rather than a missing feature.{' '}
+ Why this shape{' '}
+ works through all of them and names what to reach for instead.
+
+
+
+
+
+
+
+ {
+ [
+ { heading: 'In', nodes: inbound },
+ { heading: 'One process', nodes: core },
+ { heading: 'Out', nodes: outbound },
+ ].map((group) => (
+
+
{group.heading}
+
+ {group.nodes.map((node) => (
+ -
+
{node.label}
+ {node.detail}
+
+ ))}
+
+
+ ))
+ }
+
+
+
+ Architecture
+ ADR 005
+
+
+
+
+
+ {dedup.map((item, index) => (
+
+
+
+ ))}
+
+
+
+ Branch protection reads whichever check finished last. Holding to one
+ live run per (app, repo, sha) is what makes a required check
+ behave under force-pushes and rapid pushes — the argument is in{' '}
+ ADR 005.
+
+
+
+
+
+
+
+
+ Bindings and Apps are rows in SQLite, edited in the web UI — not a
+ block of environment variables per installation. The process reads a
+ handful of env vars so it can start; everything after that is a row.
+
+
+ A binding can override the branch list, the check name, the pipeline
+ file path, the timeout, the install / test / build commands, and
+ whether logs are shareable. At run time precedence is
+ binding → App → settings.
+
+
+ Resolution order and the full field list live in{' '}
+ the pipeline docs; the short
+ version is on /pipeline.
+
+
+
+
+
+
+
+
+ {surfaces.map((item) => (
+
+
+
+ ))}
+
+
+ The HTML UI and the JSON API come out of the same handlers, so anything
+ you can click you can also script. There is no separate frontend to
+ deploy and no second service to keep in sync.
+
+
+ API reference
+
+
+
+
+
+
+ GitHub shows the conclusion and a truncated log tail on the commit, the
+ same way it shows any other check. Steps are rendered as a table in that
+ run’s summary rather than as separate runs.
+
+
+ GET /runs/{id} on your instance. GitHub never fetches
+ it — the reader’s browser does, which is why who may open it is a{' '}
+ security question.
+
+
+ Written to a file per job on your disk, capped so one runaway build
+ cannot fill the volume, and pruned on a retention window you set.
+
+
+
+ Claims here match the published{' '}
+ security model and
+ nothing more. The page-length version is /security.
+
+
+
+
+ push event and never pull_request — a suite is already scoped to one commit and one App, so it is the unit of work.',
+ href: `${DOCS}/adr/005-check-suite-gating/`,
+ linkLabel: 'ADR 005',
+ },
+ {
+ question: 'What if a repo has no pipeline file?',
+ answer:
+ 'Commands fall back to the binding’s overrides, then to Node defaults inferred from package.json. With nothing to run at all the check reports skipped rather than failed.',
+ href: '/pipeline/',
+ linkLabel: 'Pipeline',
+ },
+ {
+ question: 'Can it re-run a job?',
+ answer:
+ 'Yes, from GitHub’s Re-run button or from the job page. A re-run supersedes anything still in flight for that commit and opens a fresh Check Run.',
+ },
+ ]}
+ />
+
+
diff --git a/src/pages/security.astro b/src/pages/security.astro
index 1071dbf..6632276 100644
--- a/src/pages/security.astro
+++ b/src/pages/security.astro
@@ -1,79 +1,225 @@
---
import MarketingPage from '../layouts/MarketingPage.astro';
+import PageHero from '../components/marketing/PageHero.astro';
+import Section from '../components/marketing/Section.astro';
+import Actions from '../components/marketing/Actions.astro';
+import CtaLink from '../components/marketing/CtaLink.astro';
+import Callout from '../components/marketing/Callout.astro';
+import Grid from '../components/marketing/Grid.astro';
+import Card from '../components/marketing/Card.astro';
+import MarkList from '../components/marketing/MarkList.astro';
+import Prose from '../components/marketing/Prose.astro';
import { CTA, DOCS, REPO } from '../lib/site';
+
+const topics = [
+ {
+ title: 'Execution',
+ body: 'Steps run as a local process, or as
docker run --rm when
runtime: is set. Job containers drop capabilities, set
no-new-privileges, and do not get the engine socket. Image names are allow-listed.',
+ },
+ {
+ title: 'Network',
+ body: 'GitHub POSTs webhooks to your public HTTPS URL, HMAC-verified against that App\u2019s secret. The worker clones with a short-lived installation token passed through
GIT_CONFIG_* as Basic
x-access-token — GitHub\u2019s git endpoint wants Basic, not the REST API\u2019s Bearer. It never enters the remote URL,
.git/config, or a command line, and the remote is removed before any pipeline step runs.',
+ },
+ {
+ title: 'Secrets at rest',
+ body: 'App PEM, webhook secret, and Coolify token columns are AES-256-GCM. GET responses return a redacted marker. The key is
CI_SECRET_KEY; rotation uses
CI_SECRET_KEY_OLD on boot.',
+ },
+ {
+ title: 'Job environment',
+ body: 'Job env is built from scratch: no
CI_SECRET_KEY, no PEMs, no webhook secrets, no Coolify tokens, no installation token.',
+ },
+ {
+ title: 'Fork pull requests',
+ body: 'Fork PRs are skipped by default. Opting in requires a reachable Docker engine and a
default_runtime. Fork jobs always run in Docker.',
+ },
+ {
+ title: 'Sessions and CSRF',
+ body: 'Session cookies are HttpOnly, and Secure behind HTTPS. Browser writes need a CSRF token; Bearer callers skip CSRF.',
+ links: [{ label: 'ADR 002', href: `${DOCS}/adr/002-authentication/` }],
+ },
+ {
+ title: 'Shareable logs',
+ body: 'A binding can opt into unauthenticated
GET /runs/{id}. Job ids are random UUIDs — treat the link as a secret.',
+ },
+];
---
- Security
- What the worker is allowed to do
-
- This page restates the published security model. It does not add claims.
- Source:
- security model
- and
- SECURITY.md.
-
-
- Execution
-
- Steps run as a local process, or as docker run --rm when
- runtime: is set. Job containers drop capabilities, set
- no-new-privileges, and do not get the engine socket. Image
- names are allow-listed.
-
-
- Network
-
- GitHub POSTs webhooks to your public HTTPS URL. The worker clones with an
- installation token via GIT_CONFIG_* Basic auth — never in the
- remote URL — then strips the remote before pipeline steps run.
-
+
+
+
+ Security model
+
+
+ SECURITY.md
+
+
+
- Secrets at rest
-
- App PEM, webhook secret, and Coolify token columns are AES-256-GCM.
- GET responses return a redacted marker. The key is CI_SECRET_KEY.
- Rotation uses CI_SECRET_KEY_OLD on boot.
-
+
+
+ {
+ topics.map((topic) => (
+
+ {topic.title}
+
+ {topic.links && (
+
+ {topic.links.map((link) => (
+
+ {link.label}
+
+ ))}
+
+ )}
+
+ ))
+ }
+
+
- Job environment
-
- Job env is built from scratch: no CI_SECRET_KEY, no PEMs, no
- webhook secrets, no Coolify tokens, no installation token.
-
+
+
+
+
+ A signed webhook for a repo with no enabled binding is acknowledged
+ and dropped, however valid its signature. Fork pull requests are
+ dropped on top of that unless you have explicitly opted in, and that
+ opt-in requires Docker.
+
+
+ Enable only repos you trust. A pipeline runs that
+ repository’s own commands — in this process by default, or in a
+ container when runtime: is set. That is the trust boundary
+ worth thinking about before you tick a box in the picker.
+
+
+ Bindings and their overrides are documented in{' '}
+ enable repos.
+
+
+
+
What a job container gets
+ no-new-privileges',
+ 'An allow-listed image name',
+ 'A fresh environment with none of your secrets in it',
+ 'No access to the Docker engine socket',
+ ]}
+ mark="check"
+ />
+
+
+
- Fork pull requests
-
- Fork PRs are skipped by default. Opt-in requires a reachable Docker engine
- and default_runtime. Fork jobs always run in Docker.
-
+
+
+
+ App PEMs, webhook secrets, and Coolify tokens are AES-256-GCM columns in
+ SQLite. Reads give back a redacted marker, never the value.
+
+
+ Set the new key, keep the old one alongside it for one start, and the
+ secret columns are re-sealed under the new key. A row that opens with
+ neither key fails startup rather than serving broken.
+
+
+ The key is the only thing that decrypts those columns — there is no
+ recovery path and no escrow. Store it where your other secrets live, and
+ see self-hosted for what
+ that means at backup time.
+
+
+
- Sessions and CSRF
-
- Session cookies are HttpOnly, Secure behind HTTPS. Browser writes need a
- CSRF token. Bearer callers skip CSRF. See
- ADR 002.
-
+
+
+ {
+ [
+ {
+ route: 'POST /webhook/{slug}',
+ who: 'GitHub only',
+ note: 'HMAC-verified against that App’s webhook secret. An unsigned or wrongly-signed delivery goes nowhere.',
+ },
+ {
+ route: 'GET /health',
+ who: 'Anyone',
+ note: 'Liveness for your proxy. 503 when SQLite cannot be read. It carries no data about your repos.',
+ },
+ {
+ route: 'GET /runs/{id}',
+ who: 'Session — or anyone, if opted in',
+ note: 'A binding can make its log pages readable by link holders. Job ids are random UUIDs; treat such a link as a secret.',
+ },
+ {
+ route: 'Everything else',
+ who: 'Session or bearer token',
+ note: 'Browser writes additionally need a CSRF token. Bearer callers carry no ambient cookie and so skip CSRF.',
+ },
+ ].map((row, i) => (
+
0 ? 'border-t border-foreground/10' : ''}`}>
+
{row.route}
+
{row.who}
+
{row.note}
+
+ ))
+ }
+
+
+
+ There are no teams, no roles, and no SSO. That is a real limit, listed
+ with the others on why this shape — not something to
+ discover after you have onboarded a team.
+
+
+
- Shareable logs
-
- A binding can opt into unauthenticated GET /runs/{id}.
- Job ids are random UUIDs; treat the link as a secret.
-
+
+
+
+ Report vulnerabilities the way SECURITY.md describes — private reporting
+ on the GitHub repository, or email. Not the public issue tracker.
+
+
+
+ SECURITY.md
+
+ security@openpreflight.xyz
+
+
+
- Reporting
-
- Report vulnerabilities as described in
- SECURITY.md
- (private reporting on the GitHub repo, and
- security@openpreflight.xyz).
-
+
+
+
+ The worker holds GitHub App private keys, which is exactly why the code
+ is public. Read what it does with them in{' '}
+ the repository, and see{' '}
+ open source for how the repos are laid out.
+
+
+
-
diff --git a/src/pages/self-hosted.astro b/src/pages/self-hosted.astro
index 0cd922f..e9415c3 100644
--- a/src/pages/self-hosted.astro
+++ b/src/pages/self-hosted.astro
@@ -1,57 +1,290 @@
---
import MarketingPage from '../layouts/MarketingPage.astro';
+import PageHero from '../components/marketing/PageHero.astro';
+import Section from '../components/marketing/Section.astro';
+import Grid from '../components/marketing/Grid.astro';
+import Card from '../components/marketing/Card.astro';
+import CodePanel from '../components/marketing/CodePanel.astro';
+import Actions from '../components/marketing/Actions.astro';
+import CtaLink from '../components/marketing/CtaLink.astro';
+import Callout from '../components/marketing/Callout.astro';
+import MarkList from '../components/marketing/MarkList.astro';
+import Faq from '../components/marketing/Faq.astro';
import { CTA, DOCS, REPO } from '../lib/site';
+
+const yours = [
+ { label: 'compose / binary', detail: 'UI · API · webhook · runner' },
+ { label: 'SQLite in DATA_DIR', detail: 'apps · bindings · jobs' },
+ { label: 'logs on disk', detail: 'served by /runs/{id}' },
+];
+
+const theirs = [
+ { label: 'HTTPS webhooks', detail: 'GitHub → your public URL' },
+ { label: 'Check Runs', detail: 'your worker → the commit' },
+ { label: 'details_url', detail: 'points back at your host' },
+];
+
+const state = [
+ {
+ path: 'DATA_DIR/ci.db',
+ holds: 'Settings, users, GitHub Apps, bindings, job rows — with the secret columns encrypted inside it.',
+ keep: 'Yes',
+ tone: true,
+ },
+ {
+ path: 'DATA_DIR/logs/',
+ holds: 'One file per job, the full build log.',
+ keep: 'If you want history',
+ tone: true,
+ },
+ {
+ path: 'CI_SECRET_KEY',
+ holds: 'The AES-256-GCM key for those encrypted columns. Store it wherever your other secrets live — not next to the backup.',
+ keep: 'Yes, separately',
+ tone: true,
+ },
+ {
+ path: 'WORKSPACE_DIR',
+ holds: 'Per-job checkouts, deleted when the job ends.',
+ keep: 'No',
+ tone: false,
+ },
+];
+
+const operate = [
+ {
+ title: 'A public HTTPS URL',
+ body: 'Something GitHub can reach, with a reverse proxy in front of port 8080.',
+ },
+ {
+ title: '
CI_SECRET_KEY',
+ body: 'The only required env var. It decrypts your secret columns — keep it forever.',
+ },
+ {
+ title: 'A volume that persists',
+ body: 'SQLite and job logs live under
DATA_DIR. Lose it and you lose history.',
+ },
+ {
+ title: 'A GitHub App you register',
+ body: 'The worker does not create Apps for you. You own it, in your org.',
+ },
+];
---
- Self-hosted
- You run the worker. GitHub shows the Check Run.
-
- There is no hosted openpreflight control plane. Compose or the binary on
- your server is the whole deployment.
-
-
- The diagram
- {`Your infra GitHub
-───────── ──────
-compose / binary
- UI + API + webhook + runner ◄── HTTPS webhooks
- SQLite in DATA_DIR ──► Check Runs
- logs on disk ──► details_url (your host)`}
-
- What you operate
-
- - A public HTTPS URL GitHub can reach (reverse proxy in front of port 8080).
- CI_SECRET_KEY — the only required env var. Keep it forever.
- - SQLite and logs under
DATA_DIR. That volume must persist.
- - A GitHub App you register. The worker does not create Apps for you.
-
-
- Walkthrough:
- quickstart
- and
- deployment.
-
-
- Docker and Coolify are optional
-
- Process executor is the default. runtime: and fork PRs need a
- Docker engine (CI_DOCKER_HOST or a mounted socket). Coolify is
- optional inventory, a repo picker, and an install-worker API — not required
- to run CI, and not a job runner.
-
-
- There is no first-class Kubernetes operator and no air-gap product. If you
- put the binary on an isolated network, that is ordinary self-hosting, not a
- documented mode.
-
-
-
+
+
+ Quickstart
+
+ Deployment
+
+
+
+
+
+
+ {
+ [
+ { heading: 'Your infrastructure', nodes: yours, accent: true },
+ { heading: 'GitHub', nodes: theirs, accent: false },
+ ].map((column) => (
+
+
+ {column.heading}
+
+
+ {column.nodes.map((node) => (
+ -
+
{node.label}
+ {node.detail}
+
+ ))}
+
+
+ ))
+ }
+
+
+
+
+
+ {
+ operate.map((item, index) => (
+
+
+
+
+
+
+
+
+ ))
+ }
+
+
+
+
+
+
CI_SECRET_KEY, generated once and kept forever',
+ ]}
+ mark="check"
+ />
+
+
What ships in the image
+
+ A static Go binary plus git, Node, and the
+ Docker CLI. It runs as an unprivileged user, and pipeline shells are
+ reaped by the init process rather than left behind.
+
+
+ Node matters only when a job has no runtime: and
+ runs as a process. A Docker engine matters only for
+ runtime: jobs and fork pull requests — the
+ service boots and reports checks without one.
+
+
+
+
+
+
+
+ {
+ state.map((row, i) => (
+
0 ? 'border-t border-foreground/10' : ''}`}>
+
{row.path}
+
{row.holds}
+
+ {row.keep}
+
+
+ ))
+ }
+
+
+
+ It restores your bindings and job history, but no credentials — the App
+ PEM and webhook secrets stay unreadable and have to be pasted in again.
+ Back both up, and back them up separately.
+
+
+
+
+
+
+
+
+
+ Nothing to clone — the file pulls the published image. Then open the
+ UI, run the first-boot wizard, register your GitHub App, and enable
+ the repos you want checks on.
+
+
+ Full quickstart
+
+ Day-two operations
+
+
+
+
+
+
+
+ cancelled, and is not retried on its own. On a kill it stays in progress and the next boot runs it again from the beginning, which opens a second Check Run on that commit.',
+ href: `${DOCS}/understanding/operations/`,
+ linkLabel: 'Operations',
+ },
+ {
+ question: 'How do upgrades work?',
+ answer:
+ 'Pull the new image and bring it up. Migrations run on boot and are forward-only — there are no down migrations, so rolling a release back is not supported and the way out of a bad upgrade is the backup you took before it.',
+ href: `${DOCS}/understanding/operations/`,
+ linkLabel: 'Operations',
+ },
+ {
+ question: 'How much machine does it need?',
+ answer:
+ 'Less than you would expect for a handful of repos, and it will not stretch further than one box. Every concurrent job holds its own checkout and its own log, so disk scales with concurrency, and there is no cap on checkout size — size the workspace for your largest repo.',
+ href: `${DOCS}/start/configuration/`,
+ linkLabel: 'Configuration',
+ },
+ {
+ question: 'What cleans up after itself?',
+ answer:
+ 'An hourly pass prunes expired sessions, then old job rows and their log files past the retention window. Queued and running jobs are never pruned. Nothing else is cleaned up automatically.',
+ href: `${DOCS}/understanding/operations/`,
+ linkLabel: 'Operations',
+ },
+ {
+ question: 'What should the proxy watch?',
+ answer:
+ 'GET /health. A 503 means the process cannot read SQLite. Honour X-Forwarded-Proto too — session and CSRF cookies set Secure when that header says https.',
+ href: `${DOCS}/understanding/deployment/`,
+ linkLabel: 'Deployment',
+ },
+ ]}
+ />
+
+
+
+
+
+ Steps run in the worker. This is what you get with no extra
+ infrastructure at all.
+
+
+ Needed for runtime: and for fork PRs, via
+ CI_DOCKER_HOST or a mounted socket.
+
+
+ Inventory, a repo picker, and an install-worker API. It never executes
+ jobs.
+
+
+
+
+ There is no first-class Kubernetes operator and no air-gap product. If
+ you put the binary on an isolated network, that is ordinary
+ self-hosting — not a documented mode with support behind it.
+
+
+
+
diff --git a/src/pages/use-cases/index.astro b/src/pages/use-cases/index.astro
index d4ba1fe..dff040c 100644
--- a/src/pages/use-cases/index.astro
+++ b/src/pages/use-cases/index.astro
@@ -1,36 +1,217 @@
---
import MarketingPage from '../../layouts/MarketingPage.astro';
-import { CTA } from '../../lib/site';
+import PageHero from '../../components/marketing/PageHero.astro';
+import Section from '../../components/marketing/Section.astro';
+import Actions from '../../components/marketing/Actions.astro';
+import CtaLink from '../../components/marketing/CtaLink.astro';
+import Callout from '../../components/marketing/Callout.astro';
+import Grid from '../../components/marketing/Grid.astro';
+import Card from '../../components/marketing/Card.astro';
+import Faq from '../../components/marketing/Faq.astro';
+import { CTA, DOCS } from '../../lib/site';
+
+const cases = [
+ {
+ href: '/use-cases/private-repos/',
+ eyebrow: 'The common one',
+ title: 'Private GitHub repos',
+ body: 'You want a status on the commit without buying hosted Actions minutes or writing workflow YAML for install / test / build.',
+ points: ['A GitHub App you register', 'Clone via installation token', 'One Check Run per commit'],
+ },
+ {
+ href: '/use-cases/self-hosted-teams/',
+ eyebrow: 'Already have the box',
+ title: 'Self-hosted teams',
+ body: 'A VPS or home-lab machine is already paid for. openpreflight is a Compose stack on it: UI, webhook, runner, SQLite, logs.',
+ points: ['Runner on your hardware', 'Logs never leave the host', 'Shareable logs are opt-in'],
+ },
+ {
+ href: '/use-cases/open-source/',
+ eyebrow: 'Proof you can click',
+ title: 'Open-source contributors',
+ body: 'The public demo repo carries pull requests that produce real Check Runs from a self-hosted instance — pass, fail, timeout, skip.',
+ points: ['Six demo pull requests', 'Public run log pages', 'Same pages you get behind auth'],
+ },
+];
---
- Use cases
- Who this is for
-
- Three honest stories. Not a policy engine, and not one YAML across GitLab
- and Jenkins.
-
-
-
-
-
+
+
+ Quickstart
+ How it works
+
+
+
+
+
+
+
+
+ The same artifact GitHub already shows for any other check, on the
+ commit and in the pull request.
+
+
+ GitHub gets a conclusion and a tail. The full log is a file on your
+ disk, served by your own instance.
+
+
+ Three shell commands in .ci.yml — or nothing at all, if a
+ Node repo’s package.json already says enough.
+
+
+
+
+
+
+ {
+ [
+ {
+ who: 'Private GitHub repos',
+ href: '/use-cases/private-repos/',
+ body: 'The repository is already on GitHub and the team wants a status on the commit. Hosted Actions would do it, but it means buying minutes for work that a machine they already pay for could do, and writing workflow YAML for what is really three commands. Registering an App and ticking a repo is a smaller ask than adopting a second configuration language.',
+ },
+ {
+ who: 'Self-hosted teams',
+ href: '/use-cases/self-hosted-teams/',
+ body: 'The constraint is not cost, it is custody: build logs should stay on infrastructure the team operates. A full platform would satisfy that and bring a control plane, agents, and a database server with it. One container and one file satisfies it too, and there is nothing else to keep patched.',
+ },
+ {
+ who: 'Open-source contributors',
+ href: '/use-cases/open-source/',
+ body: 'Nobody should have to install a CI tool to find out how it reports a failure. The demo repository carries pull requests that land on each conclusion, from an ordinary self-hosted instance, with log pages anyone can open — proof you can click before you commit an afternoon.',
+ },
+ ].map((item) => (
+
+
+ {item.who}
+
+ →
+
+
+ {item.body}
+
+ ))
+ }
+
+
+
+
+
+ {
+ [
+ {
+ case: 'Hosted CI for arbitrary public repos',
+ why: 'There is no hosted openpreflight. Every instance is one someone runs, bound to repos they chose.',
+ go: 'Read the product page',
+ href: '/product/',
+ },
+ {
+ case: 'One config across GitLab, Jenkins, and CircleCI',
+ why: '“Portable” here means the same file and the same worker on your own host — there are no vendor adapters.',
+ go: 'See integrations',
+ href: '/integrations/',
+ },
+ {
+ case: 'A policy or orchestration engine',
+ why: 'No stages, no fan-out, no cross-repo pipelines, no approvals. Actions and its neighbours own that layer.',
+ go: 'Compare the layers',
+ href: '/compare/github-actions/',
+ },
+ ].map((item) => (
+
+
+ Not this
+
+ {item.case}
+ {item.why}
+
+ {item.go}
+ →
+
+
+ ))
+ }
+
+
+
+
+
diff --git a/src/pages/use-cases/open-source.astro b/src/pages/use-cases/open-source.astro
index 5333d22..45e14c2 100644
--- a/src/pages/use-cases/open-source.astro
+++ b/src/pages/use-cases/open-source.astro
@@ -1,33 +1,223 @@
---
import MarketingPage from '../../layouts/MarketingPage.astro';
-import { CTA, DEMO_REPO } from '../../lib/site';
+import PageHero from '../../components/marketing/PageHero.astro';
+import Section from '../../components/marketing/Section.astro';
+import Grid from '../../components/marketing/Grid.astro';
+import Card from '../../components/marketing/Card.astro';
+import Actions from '../../components/marketing/Actions.astro';
+import CtaLink from '../../components/marketing/CtaLink.astro';
+import Callout from '../../components/marketing/Callout.astro';
+import Prose from '../../components/marketing/Prose.astro';
+import CodePanel from '../../components/marketing/CodePanel.astro';
+import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro';
+import { CTA, DEMO_REPO, REPO } from '../../lib/site';
+import demoRuns from '../../data/demo-runs.json';
+
+const outcomes = {
+ success: { label: 'passed', tone: 'text-primary' },
+ failure: { label: 'failed', tone: 'text-destructive' },
+ timed_out: { label: 'timed out', tone: 'text-destructive' },
+ skipped: { label: 'skipped', tone: 'text-muted-foreground' },
+};
+
+const panelSteps = [
+ { name: 'install', command: 'npm ci', duration: '7s', width: '24%' },
+ { name: 'test', command: 'npm test', duration: '11s', width: '38%', state: 'fail' as const },
+ { name: 'build', command: 'npm run build', duration: '—', width: '0%', state: 'skip' as const },
+];
---
- Use cases / Open source
- Public PRs, real Check Runs
-
- openpreflight/demo
- is a small Node utility with six pull requests. Each is meant to produce a
- Check Run on a self-hosted instance — passing, failing test, failing build,
- timeout, skipped, container runtime.
-
-
- The log pages are the same /runs/{id} pages you get
- behind auth. Shareable logs are on for that binding only. Until the demo
- App is bound, the site links the pull requests and leaves run URLs empty
- rather than inventing them.
-
-
- This is contributor-facing proof, not a hosted CI service for arbitrary
- public repos. You still run your own worker.
-
-
+
+
+ See the live runs
+ Demo repo
+
+
+
+
+
+
+ {
+ demoRuns.runs.map((entry) => {
+ const outcome = outcomes[entry.conclusion] ?? outcomes.skipped;
+ return (
+ -
+
+ {outcome.label}
+
+ {entry.title}
+ {entry.outcome}
+
+ {entry.runUrl && (
+
+ run log
+
+ )}
+
+ pull request
+
+
+
+ );
+ })
+ }
+
+
+
+
+
+
+ These checks come from a self-hosted openpreflight instance reporting
+ through its own GitHub App.
+
+
+ /runs/{'{id}'} is the page you get behind auth. Shareable
+ logs are on for that one binding.
+
+
+ This is proof you can inspect, not a hosted CI service for arbitrary
+ public repos.
+
+
+
+
+
+
+ {
+ [
+ {
+ n: '01',
+ title: 'A self-hosted instance',
+ body: 'An ordinary openpreflight box with a GitHub App registered against the demo repository. Nothing special is running.',
+ },
+ {
+ n: '02',
+ title: 'One binding, logs shareable',
+ body: 'Shareable logs are a per-binding opt-in, switched on for the demo repository only. Every other binding stays behind a session.',
+ },
+ {
+ n: '03',
+ title: 'Six branches, six conclusions',
+ body: 'The pull requests are shaped so that pass, failure, timeout, skip, and a container runtime each show up as themselves.',
+ },
+ ].map((item) => (
+
+
{item.n}
+
{item.title}
+
{item.body}
+
+ ))
+ }
+
+
+
+ Nothing about that setup is privileged. Point your own instance at a
+ public repository, opt that binding into shareable logs, and you have the
+ same thing — visibility is read from the payload but never gates
+ anything.
+
+
+
+
+
+
+
+ A conclusion and a step table in the run’s summary — install, test,
+ build, with the one that failed marked and the rest reported skipped.
+
+
+ Enough output attached to the Check Run to see what broke without
+ leaving GitHub.
+
+
+ The details URL, served by the instance that ran the job. For this
+ binding it opens without an account; everywhere else it needs a session.
+
+
+
+
+ Two of the six pull requests fail on purpose and one times out. That is
+ the part worth clicking — a green check tells you very little about
+ whether a CI tool reports failures usefully.
+
+
+
+
+
+
+
+
+ The same repository you are looking at is the one contributors work
+ in, and the demo branches double as fixtures for the behaviour they
+ exercise — a failing test, a build error, a pipeline that runs out of
+ time.
+
+
+ What the project will take, how the repositories are laid out, and why
+ the test suite needs no credentials are all on{' '}
+ open source.
+
+
+
+
Useful contributions
+
+ {
+ [
+ 'A bug report with a failing test',
+ 'A gap in v1 the code does not cover yet',
+ 'Docs corrected to match the code',
+ ].map((item) => (
+ -
+ ✓
+ {item}
+
+ ))
+ }
+
+
+ CONTRIBUTING.md →
+
+
+
+
+
+
+
+
+ The demo instance is a live box and can be down. A run URL may also 404
+ once retention prunes the job — the pull request always stays. Until the
+ demo App is bound, run URLs are left empty rather than invented.
+
+
+
+
diff --git a/src/pages/use-cases/private-repos.astro b/src/pages/use-cases/private-repos.astro
index 3553a62..6be0bd1 100644
--- a/src/pages/use-cases/private-repos.astro
+++ b/src/pages/use-cases/private-repos.astro
@@ -1,32 +1,194 @@
---
import MarketingPage from '../../layouts/MarketingPage.astro';
+import PageHero from '../../components/marketing/PageHero.astro';
+import Section from '../../components/marketing/Section.astro';
+import Grid from '../../components/marketing/Grid.astro';
+import Card from '../../components/marketing/Card.astro';
+import Steps from '../../components/marketing/Steps.astro';
+import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro';
+import Actions from '../../components/marketing/Actions.astro';
+import CtaLink from '../../components/marketing/CtaLink.astro';
+import Callout from '../../components/marketing/Callout.astro';
+import Faq from '../../components/marketing/Faq.astro';
+import MarkList from '../../components/marketing/MarkList.astro';
import { CTA, DOCS } from '../../lib/site';
+
+const panelSteps = [
+ { name: 'install', command: 'go mod download', duration: '5s', width: '16%' },
+ { name: 'test', command: 'go test ./...', duration: '24s', width: '62%' },
+ { name: 'build', command: 'go build ./...', duration: '8s', width: '22%' },
+];
+
+const steps = [
+ {
+ title: 'Register a GitHub App against those repos',
+ detail: 'It lives in your org, with the permissions the docs list — nothing broader.',
+ },
+ {
+ title: 'Bind the repos in the UI',
+ detail: 'A binding names the App, the repo, the branches, and any command overrides.',
+ },
+ {
+ title: 'Push',
+ detail:
+ 'The worker clones with an installation token that never lands in the remote URL, and strips the remote before your steps run.',
+ },
+];
---
- Use cases / Private repos
- Check Runs on private code, without Actions
-
- GitHub already stores the repo. You want a status on the commit. You do not
- want to put that work on hosted Actions minutes or write workflow YAML for
- install/test/build.
-
-
- Register a GitHub App against those private repos, bind them in the UI, and
- the worker clones with an installation token that never lands in the remote
- URL. The Check Run is the artifact GitHub already knows how to display.
-
-
- Setup:
- GitHub App
- and
- bindings.
-
-
+
+
+ Quickstart
+ How it works
+
+
+
+
+
+
+
+
+
+ GitHub already knows how to display it — on the commit, in the pull
+ request, and in branch protection.
+
+
+ The details URL is GET /runs/{'{id}'} on your own instance.
+ Nothing beyond the conclusion and a tail leaves the box.
+
+
+ .ci.yml holds the same commands you already run locally.
+
+
+
+
+
+
+
+
It can
+
+
+
+
It cannot
+
+
+
+
+
+ With a short-lived installation token that never reaches your pipeline
+ steps. The mechanics — and everything else the worker is allowed to do —
+ are on security.
+
+
+
+
+
+
+
+
+ One job produces one Check Run, and its steps are rendered as a table
+ inside that run rather than as separate checks. That is what makes it
+ usable as a required status: there is exactly one entry to tick in
+ branch protection, and adding a step to your pipeline later never
+ breaks the rule.
+
+
+ Holding to one live run per commit matters here too. Without it, a
+ force-push or a fast second commit can leave two runs of the same name
+ racing, and branch protection reads whichever finished last.
+
+
+
+
+ Pick the check name before you rely on it. GitHub
+ matches a required status check by name, so renaming one strands the
+ branch protection rule that referenced the old name. New installs
+ choose it once; an existing instance keeps the name it already has for
+ exactly this reason.
+
+
+
+
+ ADR 005
+ Check name settings
+
+
+
+
+ release/* style prefix, and an empty list means every branch the App sees.',
+ href: `${DOCS}/setup/bindings/`,
+ linkLabel: 'Bindings',
+ },
+ {
+ question: 'Can two repos run different commands?',
+ answer:
+ 'Yes. Each repo can carry its own .ci.yml, or the binding can override install, test, and build for a repo you would rather not add a file to.',
+ href: '/pipeline/',
+ linkLabel: 'Pipeline',
+ },
+ {
+ question: 'How do I try it on one repo first?',
+ answer:
+ 'Enable exactly one binding. Nothing else the App is installed on will run, because a repo with no enabled binding is ignored — so a trial stays a trial.',
+ },
+ ]}
+ />
+
+
+
+
+
+ Permissions, events, and the webhook URL to use.
+
+
+ Bindings, branch filters, and command overrides.
+
+
+
+
diff --git a/src/pages/use-cases/self-hosted-teams.astro b/src/pages/use-cases/self-hosted-teams.astro
index 3c7d1b7..c62f557 100644
--- a/src/pages/use-cases/self-hosted-teams.astro
+++ b/src/pages/use-cases/self-hosted-teams.astro
@@ -1,30 +1,187 @@
---
import MarketingPage from '../../layouts/MarketingPage.astro';
+import PageHero from '../../components/marketing/PageHero.astro';
+import Section from '../../components/marketing/Section.astro';
+import Grid from '../../components/marketing/Grid.astro';
+import Card from '../../components/marketing/Card.astro';
+import Actions from '../../components/marketing/Actions.astro';
+import CtaLink from '../../components/marketing/CtaLink.astro';
+import Callout from '../../components/marketing/Callout.astro';
+import MarkList from '../../components/marketing/MarkList.astro';
+import Steps from '../../components/marketing/Steps.astro';
+import Faq from '../../components/marketing/Faq.astro';
import { CTA, DOCS } from '../../lib/site';
+
+const stack = [
+ 'UI and JSON API',
+ 'Webhook receiver',
+ 'Job runner',
+ 'SQLite in
DATA_DIR',
+ 'Job logs on disk',
+ 'Nothing else to schedule',
+];
---
- Use cases / Self-hosted teams
- Runner and logs stay on your box
-
- The team already pays for a VPS or a home-lab machine. openpreflight is a
- Compose stack on that machine: UI, webhook, runner, SQLite, logs.
-
-
- GitHub only receives Check Run payloads and a details URL that points at
- you. Shareable logs are opt-in per binding. Operations (backups, upgrades,
- what a restart does to an in-flight job) are in
- operations.
-
-
- Coolify can inventory servers and install the worker. It is not required,
- and it does not execute jobs.
-
-
+
+
+ Self-hosting details
+ Quickstart
+
+
+
+
+
+
+
+
+ Conclusion plus a truncated log tail, written onto the commit through
+ your own GitHub App.
+
+
+ It points back at your host. The full log is served by your instance,
+ not copied anywhere.
+
+
+ Shareable logs are opt-in per binding. There is no vendor telemetry
+ endpoint and no hosted control plane.
+
+
+
+
+
+ CI_SECRET_KEY, and a volume that persists. The image pulls; there is nothing to clone.',
+ },
+ {
+ title: 'Put it behind your proxy',
+ detail:
+ 'GitHub has to reach POST /webhook/{slug} over HTTPS. Point the domain at port 8080 and let the proxy forward X-Forwarded-Proto so cookies come back Secure.',
+ },
+ {
+ title: 'Register a GitHub App and paste it in',
+ detail:
+ 'Name, slug, App ID, webhook secret, PEM. The secret columns are encrypted on the way in, and reads give back a redacted marker.',
+ },
+ {
+ title: 'Tick the repos',
+ detail:
+ 'Nothing runs for a repo you did not enable — see security for why that is the trust boundary.',
+ },
+ {
+ title: 'Push',
+ detail:
+ 'The commit collects a Check Run whose details URL points back at your box. From here it is ordinary infrastructure.',
+ },
+ ]}
+ />
+
+
+ Exact commands, mounts, and the reverse-proxy notes are in the{' '}
+ quickstart and{' '}
+ deployment pages, which
+ track the code. This page is only the shape of it.
+
+
+
+
+
+ test and build need, plus a small idle Go process. The worker itself is not the expensive part — your pipeline is.',
+ },
+ {
+ question: 'How much disk?',
+ answer:
+ 'Enough for your largest checkout, times however many jobs you allow at once, plus the logs you keep. Self-hosted has the sizing rule.',
+ },
+ {
+ question: 'How much history does it keep?',
+ answer:
+ 'A retention window you set. An hourly pass deletes job rows and their log files past it — queued and running jobs are never pruned. History is finite by design.',
+ href: `${DOCS}/understanding/operations/`,
+ linkLabel: 'Operations',
+ },
+ {
+ question: 'What does a deploy cost?',
+ answer:
+ 'A brief outage and whatever was mid-run. Self-hosted covers exactly how an interrupted job is recorded.',
+ },
+ {
+ question: 'Can jobs run on a different machine?',
+ answer:
+ 'Point CI_DOCKER_HOST at another Docker engine and runtime: jobs execute there. That is Docker’s remote API — it is still one worker deciding what runs, not a fleet.',
+ href: `${DOCS}/understanding/deployment/`,
+ linkLabel: 'Deployment',
+ },
+ ]}
+ />
+
+
+
+
+
+ There are no teams, no roles, and no SSO. One account configures Apps,
+ bindings, and settings — so decide who holds it the way you would decide
+ who holds a server credential.
+
+
+ The JSON API takes a token from a login call, so a deploy script or a
+ chat bot can list, re-run, and cancel jobs without a browser session.
+
+
+ Reading a run needs an account on your instance, with one per-repo
+ exception you can switch on. Security covers it.
+
+
+
+
+ This is a genuine limit, not an oversight, and it is listed with the
+ others on why this shape. If your team needs
+ per-person access to CI configuration, that is a reason to pick
+ something larger.
+
+
+
+
+
+
+
+ Backups, upgrades, and what a restart does to an in-flight job.
+
+
+ Reverse proxy, volumes, and the environment reference.
+
+
+
+
+ It can inventory your servers and install the worker for you.{' '}
+ Integrations covers what it does and does
+ not do here.
+
+
+
+
diff --git a/src/pages/why.astro b/src/pages/why.astro
index 6f2da0a..763fa82 100644
--- a/src/pages/why.astro
+++ b/src/pages/why.astro
@@ -1,66 +1,334 @@
---
import MarketingPage from '../layouts/MarketingPage.astro';
-import { CTA, DOCS, REPO, RELEASE } from '../lib/site';
+import PageHero from '../components/marketing/PageHero.astro';
+import Section from '../components/marketing/Section.astro';
+import Grid from '../components/marketing/Grid.astro';
+import Card from '../components/marketing/Card.astro';
+import MarkList from '../components/marketing/MarkList.astro';
+import Actions from '../components/marketing/Actions.astro';
+import CtaLink from '../components/marketing/CtaLink.astro';
+import Prose from '../components/marketing/Prose.astro';
+import Callout from '../components/marketing/Callout.astro';
+import Faq from '../components/marketing/Faq.astro';
+import { DOCS, REPO, RELEASE } from '../lib/site';
+
+const positions = [
+ {
+ eyebrow: 'One end',
+ title: 'Hosted Actions',
+ body: 'The default path. Workflow YAML, hosted minutes, and a lot of surface area.',
+ },
+ {
+ eyebrow: 'Other end',
+ title: 'A full platform',
+ body: 'Right when you need matrices, caches, and artifacts, with a control plane to operate.',
+ },
+ {
+ eyebrow: 'This',
+ title: 'In between',
+ body: 'The commit is gated, the logs stay here, and the operator is a binary.',
+ },
+];
+
+const shape = [
+ {
+ title: 'One process',
+ body: 'Configurator and worker are the same program. Nothing else to schedule.',
+ },
+ {
+ title: 'One SQLite file',
+ body: 'Apps, bindings, jobs, and encrypted secrets are rows in one file.',
+ },
+ {
+ title: 'An App you own',
+ body: 'You register the GitHub App. It is not brokered through anyone else.',
+ },
+ {
+ title: 'No new DSL',
+ body: 'Pipelines are install / test / build in
.ci.yml.',
+ },
+];
+
+const ceiling = [
+ 'GitHub Actions YAML',
+ '
actions/runner',
+ 'Creating GitHub Apps for you',
+ 'Matrices',
+ 'Caches',
+ 'Artifacts',
+];
+
+const borrowed = [
+ 'Gate on the commit, not the push',
+ 'Queue work against an immutable SHA',
+ 'Attach logs to the run',
+ 'Write the result back to the forge',
+];
+
+const rejected = [
+ 'ZooKeeper for coordination',
+ 'Nodepool for node lifecycle',
+ 'Ansible as the execution layer',
+ 'A scheduler separate from its executors',
+];
+
+const strengths = [
+ {
+ title: 'Operationally small',
+ body: 'One container, one SQLite file, one process. No broker, no agent registration, no database server.',
+ },
+ {
+ title: 'Native Check Runs',
+ body: 'Not a status API shim. Gating on the check suite is what makes required checks behave under force-pushes and rapid pushes.',
+ },
+ {
+ title: 'Configured in a UI',
+ body: 'Apps and repo bindings are rows you edit, not a block of environment variables per installation.',
+ },
+ {
+ title: 'Secrets encrypted at rest',
+ body: 'PEMs, webhook secrets, and Coolify tokens are AES-256-GCM columns.',
+ },
+ {
+ title: 'Small enough to audit',
+ body: 'The whole implementation is one
internal/ tree, with no plugin surface.',
+ },
+];
+
+const weaknesses = [
+ {
+ title: 'One machine',
+ body: 'There is no agent protocol. Jobs run in the process, or in a sibling container on the same Docker engine. This does not scale horizontally.',
+ },
+ {
+ title: 'One job at a time, by default',
+ body: '
max_concurrent_jobs is 1 and can only be raised after first boot. Raising it adds concurrency on this one machine, not across hosts.',
+ },
+ {
+ title: 'Three steps, fixed order',
+ body: 'install, test, build. No stages, no
needs:, no fan-out, no conditional steps.',
+ },
+ {
+ title: 'No caches, no artifacts',
+ body: 'Every job is a fresh shallow clone. Nothing carries between runs or moves to a later step.',
+ },
+ {
+ title: 'No matrices',
+ body: 'One pipeline per commit, not one per version combination.',
+ },
+ {
+ title: 'GitHub only',
+ body: 'It is built on Check Runs, which no other forge has.',
+ },
+ {
+ title: 'One admin user',
+ body: 'No teams, no roles, no SSO.',
+ },
+ {
+ title: 'Monorepos are the worst case',
+ body: 'There is no path filter. A commit touching one directory runs the same three commands as a commit touching all of them.',
+ },
+];
---
- Why
- CI for private repos, small enough to host
-
- The problem is not “we need another CI platform.” It is: we want a Check
- Run on our private code, on our machine, without learning a second
- workflow language.
-
-
- The problem
-
- GitHub already knows how to show a Check Run. Hosted Actions is the default
- path, and it brings YAML, hosted minutes, and a lot of surface area. Full
- self-hosted platforms exist for teams that need matrices, caches, and
- artifacts. Plenty of people want something in between: the commit is gated,
- the logs stay here, and the operator is a binary.
-
-
- v1.0.0 is out. Linux binaries are on the{" "}
- GitHub Release. The ceiling below is still the
- product boundary, not a backlog.
-
-
- The shape
-
- One process is configurator and worker. One SQLite file holds Apps,
- bindings, jobs, and encrypted secrets. You register a GitHub App you own.
- Pipelines are install/test/build in .ci.yml, not a new DSL.
- Runs are gated on the commit the way Zuul does it — trigger on the check
- suite, build the immutable SHA, one live run per commit. The ceiling of
- that model is in
- ADR 005.
-
-
- Contrast
-
- Hosted runners and full platforms solve orchestration. This product does
- not. It reports Check Runs from a worker you host. GitHub Actions can still
- orchestrate everything else. They can coexist; this does not replace
- workflow YAML. See
- openpreflight and GitHub Actions.
-
-
- The ceiling
-
- Not in v1: Actions YAML, actions/runner, creating Apps for
- you, matrices, caches, artifacts. Jobs on another machine use a Docker
- engine (CI_DOCKER_HOST), not Coolify as a job runner. If those
- are requirements, pick a different tool — the
- docs comparison is honest about
- that.
-
-
-
+
+
+ See the product
+ View source
+
+
+
+
+
+ {positions.map((position) => (
+ {position.body}
+ ))}
+
+
+
+ Both ends are the right answer for someone. Plenty of people want
+ neither: no hosted minutes to buy, no orchestration layer to run — just
+ the commit checked, on hardware that is already theirs.
+
+
+ v1.0.0 is out. Linux binaries are on the{' '}
+ GitHub Release. The ceiling below is still the
+ product boundary, not a backlog.
+
+
+
+
+
+
+ {shape.map((item) => (
+
+
+
+ ))}
+
+
+
+ Runs are gated on the commit the way Zuul does it — trigger on the
+ check suite, build the immutable SHA, one live run per commit. The
+ ceiling of that model is written down in ADR 005 rather than discovered
+ later.
+
+
+
+ Read ADR 005
+
+
+
+
+
+
+
Borrowed
+
+
+
+
Not adopted
+
+
+
+
+
+ Trigger on check_suite and check_run, never on
+ push or pull_request. A suite is already scoped
+ to one commit and one App; push would fire for refs nobody
+ is reviewing. One Check Run per job means one required-status entry in
+ branch protection, so adding a step never breaks it.
+
+
+ ADR 005 records what
+ is borrowed, what is rejected, and where the ceiling is.
+
+
+
+
+
+
+
+ Matrices, caches, artifacts, a marketplace, and a scheduler. openpreflight
+ does none of that, and GitHub Actions can keep doing all of it.
+
+
+ Clone the SHA, run install / test / build, report one Check Run. The two
+ can coexist on the same repository.
+
+
+
+
+ openpreflight and GitHub Actions
+
+
+
+
+
+
+ {strengths.map((item) => (
+
+
+
+ ))}
+
+
+
+
+
+ {weaknesses.map((item) => (
+
+ ))}
+
+
+ Not in v1 at all
+
+
+
+
+ If one of these is a dealbreaker, that is useful information rather than
+ a dead end — the comparison works
+ through which tool to reach for instead.
+
+
+
+
+
+ v1.0.0 was tagged 29 August 2026. Linux binaries are on the GitHub Release. What v1 still does not include is listed below — those things are out of scope, not unfinished.',
+ href: RELEASE,
+ linkLabel: 'GitHub Release',
+ },
+ {
+ question: 'Why gate on the check suite instead of push?',
+ answer:
+ 'Gate on the commit, not the push, without importing a second scheduler. Adding a push case would reverse that decision rather than extend it.',
+ href: `${DOCS}/adr/005-check-suite-gating/`,
+ linkLabel: 'ADR 005',
+ },
+ {
+ question: 'Why is Coolify optional?',
+ answer:
+ 'It is a deployment target and an optional repository source, not the product. Skip it and everything works the same: checks still come from an App you register, and jobs still run here or on any Docker engine you point CI_DOCKER_HOST at.',
+ href: `${DOCS}/setup/coolify/`,
+ linkLabel: 'Coolify',
+ },
+ {
+ question: 'Does it work with GitHub Enterprise Server?',
+ answer:
+ 'The plumbing is there and it has not been tested against a real instance. Each App row carries an API URL and the git origin is derived from it. What is untested is everything specific to a GHE deployment.',
+ href: `${DOCS}/start/faq/`,
+ linkLabel: 'FAQ',
+ },
+ {
+ question: 'Does the project use itself for CI?',
+ answer:
+ 'No — openpreflight/openpreflight runs GitHub Actions. A public demo instance runs Check Runs for openpreflight/demo; that is not CI for the product repo. Releases have to publish multi-arch images, which this tool does not do.',
+ href: `${DOCS}/start/faq/`,
+ linkLabel: 'FAQ',
+ },
+ ]}
+ />
+
+
diff --git a/src/styles/global.css b/src/styles/global.css
index 09cce4c..f06da8b 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -351,145 +351,129 @@ body {
font-family: var(--font-family-sans);
}
-a {
- color: inherit;
- text-decoration: none;
-}
-
-.marketing-article h1 {
- font-size: 2.25rem;
- font-weight: 600;
- letter-spacing: -0.04em;
- line-height: 1.1;
+/* In @layer base so utilities (text-primary, underline, ...) still win: an
+ unlayered rule here would override every Tailwind utility on an anchor. */
+@layer base {
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
}
-.marketing-article .lead {
- margin-top: 1.25rem;
- font-size: 1.125rem;
- line-height: 1.6;
- color: var(--muted-foreground);
-}
+/* rivelle:theme:end */
-.marketing-article h2 {
- margin-top: 2.75rem;
- font-size: 1.35rem;
- font-weight: 600;
- letter-spacing: -0.03em;
-}
+/* ------------------------------------------------------------------------
+ Marketing subpage system
+ Sections, cards, and prose that carry the landing page's visual language
+ into /product, /why, /security and the rest. Tokens only — no second accent.
+ ------------------------------------------------------------------------ */
-.marketing-article h3 {
- margin-top: 1.75rem;
- font-size: 1.05rem;
- font-weight: 600;
+/* Softer relative of .hero-atmosphere for inner-page heroes. */
+.page-atmosphere {
+ background-image:
+ radial-gradient(
+ ellipse 70% 60% at 50% 0%,
+ color-mix(in srgb, var(--primary) 16%, transparent),
+ transparent 62%
+ ),
+ linear-gradient(
+ to right,
+ color-mix(in srgb, var(--foreground) 4%, transparent) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ color-mix(in srgb, var(--foreground) 4%, transparent) 1px,
+ transparent 1px
+ );
+ background-size: auto, 48px 48px, 48px 48px;
+ mask-image: linear-gradient(to bottom, black 40%, transparent);
}
-.marketing-article p,
-.marketing-article ul,
-.marketing-article ol {
- margin-top: 0.85rem;
- line-height: 1.65;
+/* Reading-width body copy inside a section. design.md: reading-max 42rem. */
+.prose-block {
+ max-width: 42rem;
+ font-size: 1rem;
+ line-height: 1.7;
color: var(--muted-foreground);
}
-.marketing-article ul,
-.marketing-article ol {
- padding-left: 1.2rem;
+.prose-block > * + * {
+ margin-top: 1rem;
}
-.marketing-article li + li {
- margin-top: 0.4rem;
+.prose-block strong {
+ color: var(--foreground);
+ font-weight: 600;
}
-.marketing-article a {
+.prose-block a {
color: var(--primary);
+ text-decoration: underline;
+ text-decoration-color: color-mix(in srgb, var(--primary) 35%, transparent);
text-underline-offset: 4px;
}
-.marketing-article a:hover {
- text-decoration: underline;
+.prose-block a:hover {
+ text-decoration-color: currentColor;
}
-.marketing-article pre,
-.marketing-article .diagram {
- margin-top: 1.25rem;
- overflow-x: auto;
- border: 1px solid var(--border);
- border-radius: 0.75rem;
- padding: 1rem 1.1rem;
+.prose-block code,
+.inline-code {
font-family: var(--font-mono);
- font-size: 0.8rem;
- line-height: 1.55;
+ font-size: 0.85em;
+ border-radius: var(--radius-sm);
+ padding: 0.1em 0.35em;
+ background: color-mix(in srgb, var(--foreground) 6%, transparent);
color: var(--foreground);
}
-.marketing-article .kicker {
- font-family: var(--font-mono);
- font-size: 0.875rem;
- font-weight: 500;
- letter-spacing: 0.04em;
- color: var(--primary);
+.prose-block ul,
+.prose-block ol {
+ padding-left: 1.15rem;
+ list-style-position: outside;
}
-.marketing-article .cta-row {
- display: flex;
- flex-wrap: wrap;
- gap: 0.75rem;
- margin-top: 2.5rem;
+.prose-block ul {
+ list-style-type: disc;
}
-.marketing-article .cta-row a {
- display: inline-flex;
- align-items: center;
- border-radius: 0.375rem;
- padding: 0.5rem 1rem;
- font-size: 0.875rem;
- font-weight: 500;
- text-decoration: none;
-}
-
-.marketing-article .cta-row a.primary {
- background: var(--primary);
- color: var(--primary-foreground);
+.prose-block ol {
+ list-style-type: decimal;
}
-.marketing-article .cta-row a.secondary {
- border: 1px solid var(--border);
- color: var(--foreground);
+.prose-block li + li {
+ margin-top: 0.45rem;
}
-.marketing-article .card-grid {
- display: grid;
- gap: 0.85rem;
- margin-top: 1.5rem;
+/* Copy that sits on the inverted (bg-foreground) band. */
+.on-dark .prose-block {
+ color: color-mix(in srgb, var(--background) 62%, transparent);
}
-@media (min-width: 640px) {
- .marketing-article .card-grid.cols-2,
- .marketing-article .card-grid.cols-3 {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
+.on-dark .prose-block strong,
+.on-dark .prose-block code {
+ color: var(--background);
}
-.marketing-article .card-grid a,
-.marketing-article .card-grid div {
- border: 1px solid var(--border);
- border-radius: 1rem;
- padding: 1.1rem 1.2rem;
- color: inherit;
- text-decoration: none;
+.on-dark .prose-block code {
+ background: color-mix(in srgb, var(--background) 12%, transparent);
}
-.marketing-article .card-grid a:hover {
- border-color: var(--primary);
- text-decoration: none;
+.on-dark .prose-block a {
+ color: var(--primary);
}
-.marketing-article .card-grid h3 {
- margin-top: 0;
+/* Wire diagrams: mono labels in boxes, wrapping instead of overflowing the
+ way the old ASCII art did on a phone. */
+.wire-node {
+ border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
+ border-radius: var(--radius-lg);
+ background: var(--card);
+ font-family: var(--font-mono);
}
-.marketing-article .card-grid p {
- margin-top: 0.4rem;
- font-size: 0.9rem;
+.on-dark .wire-node {
+ border-color: color-mix(in srgb, var(--background) 14%, transparent);
+ background: color-mix(in srgb, var(--background) 6%, transparent);
}
-/* rivelle:theme:end */