Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Deploy Documentation

# Publish docs when a version is released (changesets creates a GitHub Release on publish), so the
# site documents the latest *released* library — and also when the docs themselves change on main
# (theme bumps, content edits), since those need no library release to be worth shipping.
on:
release:
types: [published]
push:
branches: [main]
paths:
- "docs/**"
- "pnpm-workspace.yaml"
- ".github/workflows/deploy-docs.yml"
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
# Collapse a burst of Release events for the same commit into a single Pages deploy — the latest
# run cancels earlier ones.
group: pages
cancel-in-progress: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- name: Setup
uses: ./.github/actions/setup

- name: Build documentation
run: pnpm --filter ./docs exec turbo build
Comment thread
btravers marked this conversation as resolved.

- name: Upload artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: docs/.vitepress/dist

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

Incubating. All packages are currently `private` while the API settles.

**Docs:** https://btravstack.github.io/start/

## The idea

Your application is a **demesne graph** — pure use cases behind ports, adapters wired at the
Expand Down
96 changes: 96 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { defineConfig } from "vitepress";

const SITE_DESCRIPTION =
"Type-safe, 12-factor backend applications for TypeScript: one demesne graph, many transport hosts. A contract, a handler, and a host per invocation — HTTP, AMQP and Temporal over one kernel. Incubating.";

// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "start",
description: SITE_DESCRIPTION,
base: "/start/",
lang: "en-US",
cleanUrls: true,

sitemap: {
hostname: "https://btravstack.github.io/start/",
},
Comment thread
btravers marked this conversation as resolved.

themeConfig: {
logo: { light: "/logo-light.svg", dark: "/logo-dark.svg" },

nav: [
{ text: "Guide", link: "/guide/the-idea" },
{
text: "Design RFCs",
link: "https://github.com/btravstack/start/tree/main/design",
},
// Back to the btravstack hub (links the docs up to the landing page).
{ text: "btravstack", link: "https://btravstack.github.io/" },
],

sidebar: {
"/guide/": [
{
text: "Introduction",
items: [
{ text: "The idea", link: "/guide/the-idea" },
{ text: "Status", link: "/guide/status" },
],
},
{
text: "The pieces",
items: [
{ text: "Hosts", link: "/guide/hosts" },
{ text: "Packages", link: "/guide/packages" },
{ text: "Examples", link: "/guide/examples" },
],
},
],
},

socialLinks: [{ icon: "github", link: "https://github.com/btravstack/start" }],

footer: {
message: "Released under the MIT License. Incubating — the API is still settling.",
copyright: `Copyright © ${new Date().getFullYear()} Benoit TRAVERS`,
},

search: {
provider: "local",
},

editLink: {
pattern: "https://github.com/btravstack/start/edit/main/docs/:path",
text: "Edit this page on GitHub",
},
},

vite: {
// @btravstack/theme's entry imports `vitepress/theme` (which pulls in `.css`)
// and its own `style.css`. VitePress externalizes node_modules deps in the SSR
// build, so Node's ESM loader would hit those `.css` files and throw
// ERR_UNKNOWN_FILE_EXTENSION. Bundling the theme through Vite handles the CSS.
ssr: { noExternal: ["@btravstack/theme"] },
},

head: [
["link", { rel: "icon", type: "image/svg+xml", href: "/start/logo.svg" }],
["meta", { name: "author", content: "Benoit TRAVERS" }],
["meta", { name: "robots", content: "index, follow" }],
[
"meta",
{
name: "keywords",
content:
"typescript, backend, framework, 12-factor, hexagonal, dependency injection, hono, amqp, rabbitmq, temporal, demesne, unthrown, type-safe",
},
],
["meta", { property: "og:type", content: "website" }],
["meta", { property: "og:site_name", content: "start" }],
["meta", { property: "og:title", content: "start" }],
["meta", { property: "og:description", content: SITE_DESCRIPTION }],
["meta", { property: "og:image", content: "https://btravstack.github.io/start/og-start.png" }],
["meta", { name: "twitter:card", content: "summary_large_image" }],
["meta", { name: "twitter:image", content: "https://btravstack.github.io/start/og-start.png" }],
],
});
77 changes: 77 additions & 0 deletions docs/.vitepress/theme/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* start accent — cyan. The shared @btravstack/theme derives every accent
* shade (deep/hover/soft/washes/hero glow/feature icons/AA text) from this
* single token. */
:root {
--accent: #22b8cf;
}

/* Hero name in the package accent — the BtravStack multi-accent rule: the
* canvas stays neutral, the product glows in its own color (AA via
* --text-accent, which darkens on light). */
:root:root {
--vp-home-hero-name-color: var(--text-accent);
}

/* Home-hero background glyph — the project's motif behind the hero (a play
* button and a launch trajectory: start). Painted in --accent through a mask,
* so it always tracks the accent token; the SVG is base64-encoded so no CSS
* minifier can mangle it. */
.VPHome {
position: relative;
/* content-column width, and the gutter from the viewport edge in to it —
defined once so the glow and glyph stay anchored to the same column. */
--btv-col: 1152px;
--btv-gutter: max(0px, calc((100% - var(--btv-col)) / 2));
}
.VPHome::after {
content: "";
position: absolute;
top: 8px;
left: var(--btv-gutter);
right: var(--btv-gutter);
aspect-ratio: 500 / 300;
background-color: var(--accent);
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgMzAwIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMzAsMjQwIEg1MDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIi8+PGNpcmNsZSBjeD0iMTQwIiBjeT0iMTUwIiByPSI3MCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNMTE4LDExMiBMMTg4LDE1MCBMMTE4LDE4OCBaIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjxwYXRoIGQ9Ik0yNDAsMjM2IEMzMjAsMjEwIDM5MCwxNTAgNDU4LDY0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWRhc2hhcnJheT0iMTAgMTIiLz48cGF0aCBkPSJNNDQ2LDg2IEw0NjQsNTYgTTQzNCw2NCBMNDY4LDc2IiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjwvc3ZnPgo=")
no-repeat top center / contain;
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgMzAwIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMzAsMjQwIEg1MDAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIi8+PGNpcmNsZSBjeD0iMTQwIiBjeT0iMTUwIiByPSI3MCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNMTE4LDExMiBMMTg4LDE1MCBMMTE4LDE4OCBaIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjxwYXRoIGQ9Ik0yNDAsMjM2IEMzMjAsMjEwIDM5MCwxNTAgNDU4LDY0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWRhc2hhcnJheT0iMTAgMTIiLz48cGF0aCBkPSJNNDQ2LDg2IEw0NjQsNTYgTTQzNCw2NCBMNDY4LDc2IiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjwvc3ZnPgo=")
no-repeat top center / contain;
opacity: 0.13;
pointer-events: none;
z-index: 0;
}
@media (max-width: 768px) {
.VPHome::after {
display: none;
}
}

/* The shared theme only enlarges the hero mark at >=960px; below that the
* mark's small intrinsic size leaves it tiny, so size it up for tablet/mobile
* too. */
@media (max-width: 959px) {
.VPHero .image-src {
width: min(208px, 62vw);
height: min(208px, 62vw);
object-fit: contain;
}
}

/* Under-construction strip — the worker beet on site beneath the hero. */
.btv-wip {
display: flex;
align-items: center;
gap: 18px;
margin: 12px 0 28px;
padding: 16px 20px;
border: 1.5px dashed color-mix(in srgb, var(--accent) 40%, transparent);
border-radius: 14px;
}
.btv-wip img {
flex: none;
width: 64px;
height: 72px;
}
.btv-wip p {
margin: 0;
line-height: 1.55;
}
4 changes: 4 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Theme from "@btravstack/theme";
import "./custom.css";

export default Theme;
14 changes: 14 additions & 0 deletions docs/guide/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Examples

Each example in [`examples/`](https://github.com/btravstack/start/tree/main/examples) is a
complete application with a `test:integration` suite that runs against **real infrastructure**
via [testcontainers](https://testcontainers.com):

- **`start-api-todo`** — plain HTTP + in-memory repo (the base example).
- **`start-orpc-prisma`** — a typed [oRPC](https://orpc.unnoq.com) API backed by Prisma/Postgres.
- **`start-amqp-worker`** — a RabbitMQ consumer via a real amqplib driver.
- **`start-temporal-worker`** — a Temporal worker + deterministic workflow.

The examples are the executable form of the design RFCs: one demesne graph per app, served through
a host, with config, wiring, errors and shutdown shared. While the packages are private, the
examples are the best way to read how start feels to use.
30 changes: 30 additions & 0 deletions docs/guide/hosts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Hosts

A **host** owns the process for one transport: it opens the listener or consumer, opens a demesne
fork scope per invocation, runs the handler through the kernel's `runHandler`, and maps the
outcome to the transport's vocabulary through a total `DispositionMap`.

Hosts contain **no DI, lifecycle, validation, or dispatch logic of their own** — that all lives in
the kernel, once. A host is pure transport glue.

| Host | Transport | Disposition `D` | Invocation unit |
| ---------------------------- | ------------------------------- | --------------------------- | --------------- |
| `@btravstack/start-api` | HTTP ([Hono](https://hono.dev)) | HTTP status | per request |
| `@btravstack/start-amqp` | AMQP / RabbitMQ | ack / requeue / dead-letter | per message |
| `@btravstack/start-temporal` | [Temporal](https://temporal.io) | retryable / non-retryable | per activity |

## HTTP — `@btravstack/start-api`

`createHttpApp` + `httpListener`: contracts become routes, handler outcomes become statuses. The
listener owns startup and graceful shutdown of the Node server.

## AMQP — `@btravstack/start-amqp`

Settlement dispositions (ack, requeue, dead-letter) over a wire `AmqpDriver`, with an optional
`IdempotencyStore` for redelivery — a message seen twice runs the use case once.

## Temporal — `@btravstack/start-temporal`

Activities as demesne-wired use cases. Determinism is enforced **structurally**: workflows receive
activity proxies, never a container, so a workflow cannot accidentally reach non-deterministic
code.
26 changes: 26 additions & 0 deletions docs/guide/packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Packages

All packages are currently `private` while the API settles — see [Status](/guide/status).

## `@btravstack/start-kernel`

The spine. Everything the hosts share lives here, once:

- `defineConfig` — environment read and validated at the boundary (factor III)
- `runHost` — process ownership and graceful shutdown (factor IX)
- `defineContract` / `handler` / `runHandler` — the contract-to-use-case binding
- `DispositionMap` / `dispatch` — total outcome-to-transport mapping

## `@btravstack/start-api`

HTTP host on [Hono](https://hono.dev): `createHttpApp` + `httpListener`.

## `@btravstack/start-amqp`

AMQP host: settlement dispositions over a wire `AmqpDriver`, with an optional `IdempotencyStore`
for redelivery.

## `@btravstack/start-temporal`

Temporal host: activities as demesne-wired use cases; determinism enforced structurally —
workflows get activity proxies, never a container.
26 changes: 26 additions & 0 deletions docs/guide/status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Status

<div class="btv-wip">
<img src="/beet-worker.svg" alt="A beetroot in a hard hat, digging" />
<p><strong>start is incubating.</strong> All packages are currently <code>private</code> while the API settles — nothing is published to npm yet, and everything may change without ceremony.</p>
</div>

## What that means in practice

- **No install instructions.** The `@btravstack/start-*` packages are not on npm. When the kernel
API stabilises, they will be published and this site will grow real getting-started guides.
- **The design RFCs are the source of truth.** The documents in
[`design/`](https://github.com/btravstack/start/tree/main/design) are invariant-driven and
reviewed before code; when the code and an RFC disagree, the RFC wins until amended.
- **The examples are real.** Each example application in the repo ships a `test:integration`
suite that runs against real infrastructure (Postgres, RabbitMQ, Temporal) via
[testcontainers](https://testcontainers.com) — the design is exercised end-to-end on every CI
run, even while incubating.

## Following along

Watch the [GitHub repository](https://github.com/btravstack/start) for progress. The rest of the
stack — [amqp-contract](https://btravstack.github.io/amqp-contract/),
[temporal-contract](https://btravstack.github.io/temporal-contract/),
[unthrown](https://btravstack.github.io/unthrown/) and
[demesne](https://btravstack.github.io/demesne/) — is published and documented today.
49 changes: 49 additions & 0 deletions docs/guide/the-idea.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# The idea

Your application is a **demesne graph** — pure use cases behind ports, adapters wired at the
boundary, everything discharged before you run. start adds the process spine and lets you serve
that _same graph_ over any transport.

Every invocation, whatever the transport, is the same triple:

> a **contract** (zod input / output) + a **handler** (a demesne-injected use case) + a **host**
> (owns the process, opens a fork scope per invocation)

The host is the only thing that changes between transports. Config, the DI graph, the error
channel, and graceful shutdown are shared.

## Why "12-factor falls out"

The kernel makes the classic [12-factor](https://12factor.net) disciplines structural rather than
aspirational:

- **Factor III (config)** — `defineConfig` reads the environment once, validated, at the boundary.
Nothing deeper in the graph touches `process.env`.
- **Factor IX (disposability)** — `runHost` owns startup and graceful shutdown. Resources are
scoped by demesne and released LIFO; a fork scope opens per invocation and closes with it.
- **Ports and adapters** — use cases stay pure behind `Context<R>` signatures; transports are
adapters at the edge, not callers woven through your domain.

## Dispositions

A handler doesn't know its transport, so it can't speak HTTP statuses or AMQP acks. It returns a
typed outcome; each host maps outcomes to its transport's vocabulary through a **total
`DispositionMap`** — total meaning the compiler insists every outcome is mapped:

| Host | Disposition `D` | Invocation unit |
| ---------------------------- | --------------------------- | --------------- |
| `@btravstack/start-api` | HTTP status | per request |
| `@btravstack/start-amqp` | ack / requeue / dead-letter | per message |
| `@btravstack/start-temporal` | retryable / non-retryable | per activity |

## Relationship to demesne

[demesne](https://btravstack.github.io/demesne/) is a standalone, unbranded DI library; start
depends on it as a published package — a one-way dependency (demesne knows nothing of start). They
evolve independently.

## Where the design lives

The RFCs in [`design/`](https://github.com/btravstack/start/tree/main/design) are the source of
truth (invariant-driven): the thesis, the kernel API, and the handler binding. While start is
incubating, read those before reading code.
Loading
Loading