Examples
Python booking app
Fern Studio, a class-booking app in two parts: a SvelteKit site on a FastAPI JSON API. Browse the timetable, pay with Stripe Checkout, get a confirmation through Resend, see your bookings. Postgres through psycopg.
Fern Studio, a class-booking app in two parts: a SvelteKit site on a FastAPI JSON API. Browse the timetable, pay with Stripe Checkout, get a confirmation through Resend, see your bookings. Postgres through psycopg. See it on GitHub.
What it shows
- Two apps in two languages behind one proxy: stackEnvironment starts the API, then the site, and wires each to the other's address
- An app outside Node behind danwo's proxy: the official Stripe and Resend Python SDKs reach the emulators through the proxy and CA variables danwo sets, with no test hooks
- Every hop on the record: the browser, the SvelteKit server calling the API, the browser calling the API, and the API calling Stripe and Resend
- Invariants that read Postgres against Stripe and Resend: one booking per paid session, one confirmation per booking, amounts equal to what Stripe charged
danwo matrixfaulting Stripe, Resend, the webhooks and Postgres itself (54 trials), and the app surviving every one- A
STUDIO_BUGswitch that turns one safeguard off, so you can watch the matrix catch it
Stack
PythonFastAPISvelteKitPostgresuvEmulates: Stripe, Resend. Runs on: A FastAPI API started with uv and a SvelteKit site on Node, in Chromium, with Postgres in Docker.
Run it
Copy examples/with-python anywhere, then from its folder:
npm install
npm testIts scripts:
npm run build # cd web && vite build
npm run typecheck # cd web && svelte-kit sync && svelte-check --fail-on-warnings && cd .. && tsc --noEmit
npm test # danwo test
npm run test:agentic # danwo test tests/agentic.danwo.ts
npm run matrix # danwo matrix tests/deterministic.danwo.ts --grep "books a class"
npm run matrix:plan # danwo matrix tests/deterministic.danwo.ts --grep "books a class" --plan
npm run serve # danwo serveIts README says what else it needs and what the last full matrix found.
Until the danwo version it pins is on npm, install it from a local build instead: from the repository's root, bun run release <version> --pack, then node examples/link.ts with-python, which installs a copy and prints where it is.
Apps in other languages explains what this example leans on.
The test
import { resendInbox, test as base } from "@danwo/sdk"
import {
CUSTOMER,
DECLINED_CARD,
expectBooksBalance,
expectOneSessionPerBooking,
pay,
type StudioFixtures,
} from "./studio.ts"
const test = base.extend<StudioFixtures>()
test("browses the timetable", async ({ app, screen, expect }) => {
await app.open("/")
await expect(screen.getByRole("heading", { name: "This week's classes" })).toBeVisible()
await expect(screen.getByRole("link", { name: /Spin sprint/ })).toContainText("Full")
await screen.getByRole("link", { name: /Pottery wheel/ }).click()
await expect(screen.getByRole("heading", { name: "Pottery wheel" })).toBeVisible()
await expect(screen.getByTestId("class-price")).toHaveText("$45.00")
await expect(screen.getByRole("button", { name: "Book and pay $45.00" })).toBeVisible()
}).invariant(async (fixtures) => {
// Looking is not booking: nothing is paid, so nothing is owed.
await expectBooksBalance(fixtures)
fixtures.expect(await fixtures.sql("select id from bookings")).toHaveLength(0)
})
test("books a class and pays through Stripe Checkout", async ({ app, screen, expect }) => {
await pay({ app, screen, expect }, { klass: "sunrise-flow", email: "[email protected]" })
await expect(screen.getByRole("heading", { name: "You're booked" })).toBeVisible()
await expect(screen.getByTestId("booking-total")).toHaveText("$18.00")
await app.open("/")
await expect(screen.getByRole("link", { name: /Sunrise flow/ })).toContainText("11 places left")
}).invariant(async (fixtures) => {
await expectBooksBalance(fixtures)
await expectOneSessionPerBooking(fixtures)
})
test("a declined card shows Stripe's error and books nothing", async ({ app, screen, expect }) => {
await pay(
{ app, screen, expect },
{ klass: "breathwork", email: "[email protected]", card: DECLINED_CARD },
)
await expect(screen.getByRole("alert")).toHaveText("Your card was declined.")
await app.open("/bookings?email=kit%40fernstudio.test")
await expect(screen.getByText("No bookings yet")).toBeVisible()
}).invariant(async (fixtures) => {
await expectBooksBalance(fixtures)
fixtures.expect(await fixtures.sql("select id from bookings")).toHaveLength(0)
fixtures.expect(resendInbox(fixtures.resend).list("[email protected]")).toHaveLength(0)
})
test("lists my bookings", { session: "returning-customer" }, async ({ app, screen, expect }) => {
await app.open("/")
await screen.getByRole("link", { name: "My bookings" }).click()
const row = screen.getByRole("row", { name: /Sunrise flow/ })
await expect(row).toContainText("$18.00")
await expect(row).toContainText("Sent")
// Refresh asks the API from the browser, not through the site's server.
await screen.getByRole("button", { name: "Refresh" }).click()
await expect(screen.getByRole("status")).toHaveText("Up to date")
await expect(row).toContainText("Sunrise flow")
}).invariant(async (fixtures) => {
await expectBooksBalance(fixtures)
fixtures.expect(await fixtures.sql("select email from bookings")).toEqual([{ email: CUSTOMER }])
})