---
title: "Day one: from empty repo to a deployed Worker"
date: 2026-08-01
summary: >-
  Ursprung went from an empty repository to a Bun monorepo that lints,
  type-checks, publishes to npm with provenance, and deploys itself to
  Cloudflare on every push. Along the way it picked up Wrangler's experimental
  TypeScript configuration, a release-tag guard on publishing, and 22 vendored
  agent skills. The Worker still returns "Hello, world!" — the plumbing got
  built first, on purpose.
---

# Day one: from empty repo to a deployed Worker

Ursprung started this morning with an empty repository and ended the day as a
Bun monorepo that lints, type-checks, publishes to npm with provenance, and
deploys itself to Cloudflare on every push. Here is what went in, roughly in the
order it happened.

## The workspace

The foundation is a Bun workspace with two globs: `apps/*` for things you can
run and `packages/*` for things you can import. The first inhabitants are
`packages/ursprung`, which currently exports a single `name` constant, and
`apps/web`, the site you are reading this on.

Tooling is deliberately minimal and all from the Oxc family: `oxlint` for
linting and `oxfmt` for formatting, with TypeScript doing the type checking. A
single `bun run check` runs all three, which keeps the local loop and the CI
definition honest about being the same thing.

The TypeScript configuration is strict in the ways that catch real bugs rather
than the ways that generate busywork — `strict`, plus `noUncheckedIndexedAccess`,
`noFallthroughCasesInSwitch`, and unused local/parameter checks. Modules are set
to `Preserve` with `allowImportingTsExtensions`, so imports keep their `.ts`
extensions and Bun and Wrangler resolve them directly. There is no build step to
speak of, and that is the point: `packages/ursprung` sets `"exports":
"./src/index.ts"` and ships source.

## The web app

`apps/web` is a Cloudflare Worker. The handler is four lines long right now, but
the interesting part is the static assets directory — `public/` is served
alongside the Worker, which is where these posts live as plain Markdown files.

It started the day as a conventional `wrangler.jsonc`, and was migrated a few
hours later to Wrangler's experimental TypeScript configuration. That migration
splits one JSON file into two typed ones along a seam that turns out to be
meaningful:

- `cloudflare.config.ts` describes the Worker at runtime — its name, entrypoint,
  compatibility date, and domains.
- `wrangler.config.ts` describes the tooling around it — right now just the
  static assets directory.

The entrypoint is imported with an import attribute (`with { type: "cf-worker" }`)
rather than named as a string path, so a typo in the entrypoint is a type error
instead of a deploy-time surprise. The package scripts opt in with
`--x-new-config`. The API is experimental, so the Wrangler version range stays
deliberate — that is a known cost that was taken knowingly.

Late in the day the Worker picked up `ursprung.dev` as a custom domain, while
keeping `workers.dev` and preview URLs enabled so pull requests still get their
own deployable URL.

Deployment itself is handed to Cloudflare Workers Builds rather than a GitHub
Action. Cloudflare owns the Git connection and credentials; the repo keeps the
Worker configuration in source control. The build watch paths are scoped to
`apps/web`, `packages/ursprung`, and the lockfile-adjacent root files, so an
unrelated change to a package the web app does not import will not trigger a
deploy.

## CI and publishing

Two GitHub Actions workflows landed today.

`check.yml` runs on every push and pull request: install with a frozen lockfile,
check formatting, lint. Actions are pinned to exact versions and Node is on
`lts/*`.

`publish.yml` is the more opinionated one. It fires on published releases and
pushes `packages/ursprung` to npm with `--provenance`, which means the published
tarball carries a signed, verifiable link back to the workflow run and commit
that produced it. Getting that requires `id-token: write` permissions and a
current npm, so the job updates npm before publishing. There is also a small
guard that has saved more people than it sounds like it would: before publishing,
the workflow asserts that the release tag equals `v` plus the version in
`package.json`, and fails the job otherwise. Concurrency is grouped per release
tag with `cancel-in-progress: false`, because a half-cancelled publish is worse
than a slow one.

`packages/ursprung` moved from `0.0.0` to `0.0.3` over the course of the day as
the publishing path was exercised.

## Guardrails and agent skills

The last two changes were about the day-to-day feel of working in the repo.

Husky now runs `bun run lint && bun run format:check` as a pre-commit hook, so
the fast half of CI runs before a commit exists rather than after it is pushed.
Type checking is deliberately left to CI — it is the slow one, and a pre-commit
hook that people start bypassing is worse than no hook.

The repo also vendors 22 agent skills from
[mattpocock/skills](https://github.com/mattpocock/skills) into `.agents/skills`
— things like `code-review`, `diagnosing-bugs`, `domain-modeling`, `tdd`, and
`wayfinder`. They are checked in rather than fetched on demand, with a
`skills-lock.json` recording each skill's source, path, and content hash, and a
`bun run skills:update` script to refresh them. Checking them in means the agent
behaviour in this repo is reviewable in a diff like any other dependency.

That last change had an immediate consequence: the vendored skills are Markdown
and shell scripts written by someone else, and running them through this repo's
formatter is neither useful nor polite. `.agents/skills/**` is now in `oxfmt`'s
ignore patterns.

## Where this leaves things

At the end of day one the shape is: a Bun monorepo with one library and one
Worker, checks that run identically on a laptop and in CI, a publishing path with
provenance and a version guard, and a site that deploys from `main` with preview
URLs on branches. The Worker still returns "Hello, world!" — the plumbing got
built first, on purpose. The next interesting question is what actually renders
these posts.
