Skip to content

Develop

This page covers work on HappyView itself: choosing a base branch, setting up a checkout, running the development stack, and what a pull request needs before it can merge. To run the checks CI runs, see Test. To build an app on HappyView instead, see Installation.

BranchWhat it carriesTarget it when
betaThe current release line, v2The change belongs to v2
alphaThe next major line, v3, which this documentation describesThe change belongs to v3
mainThe stable v2 releaseNever directly: main is updated from beta when a release is cut

GitHub opens a new pull request against main by default, so set the base to beta or alpha.

You need:

  • rustup. rust-toolchain.toml pins Rust 1.97.1 with clippy and rustfmt, and rustup uses that version inside the checkout; see rustup’s toolchain file documentation.
  • Node.js 24 and npm, for the dashboard in web/.
  • Bun, for the JavaScript SDK packages in packages/.
  • Docker with Compose, for the development stack and the test databases.
  1. Fork the repository on GitHub and clone your fork. Then add the main repository as upstream:

    git
    git remote add upstream https://github.com/gamesgamesgamesgamesgames/happyview.gitgit fetch upstream
  2. Start a branch from the base you chose:

    git
    git switch -c fix/my-change upstream/alpha

    The commands on this page use alpha. For a v2 change, use beta in its place.

  3. Point git at the repository’s .githooks/ directory, once per clone:

    git
    git config core.hooksPath .githooks

Git then runs a subset of CI’s checks before each commit and push:

Git eventRuns
pre-commitcargo fmt -- --check and cargo clippy -- -D warnings over the library and binaries, then Prettier on the staged files under web/
pre-pushcargo clippy --all-targets -- -D warnings, which also lints the test code

The Prettier step restages the files it formats. It leaves alone any file that is staged and also modified in the working tree, and it is skipped while web/node_modules/.bin/prettier is missing, so run npm ci in web/ on the host. The development stack installs the dashboard’s dependencies in a container volume, which doesn’t create the host’s web/node_modules. The two clippy runs build into target/clippy, so clippy and cargo test don’t invalidate each other’s build cache.

docker-compose.yml runs the server and the dashboard, and rebuilds each when its source changes.

  1. Copy the example environment file:

    cp
    cp .env.example .env
  2. Set SESSION_SECRET in .env to a random value of at least 32 bytes. This command prints one:

    openssl
    openssl rand -base64 48

    Without a secure value the server starts, but cookie-based sign-in to the dashboard is disabled.

  3. Start the stack:

    docker
    docker compose up

    The first run compiles the Rust workspace inside the container, which takes several minutes. Later runs reuse the cached build volume.

  4. Open http://127.0.0.1:3080.

ServicePortWhat it runs
caddy3080The single origin. It sends the server’s paths, such as /api, /admin, /xrpc and /oauth, to happyview, and everything else to web
happyview3000The Rust server under cargo watch
web3001The Next.js dashboard dev server
tunnelnonecloudflared, which gives the stack a public HTTPS URL

Use port 3080. Opening 3000 or 3001 directly splits the session cookie across two origins and bypasses Caddy’s routing. Port 3000 does serve a dashboard, but not this stack’s: the happyview service sets no STATIC_DIR. It serves web/out instead, which holds whatever your last local npm run build left there, or nothing.

Configuration lists every variable .env can set.

The stack uses SQLite. To use Postgres, uncomment three blocks in docker-compose.yml: the postgres service, its pgdata volume, and the depends_on block on the happyview service. Then set these in .env:

Terminal
DATABASE_URL=postgres://happyview:happyview@postgres/happyviewPOSTGRES_USER=happyviewPOSTGRES_PASSWORD=happyviewPOSTGRES_DB=happyview

Database setup covers the differences between the two backends.

To run the server with cargo run and no containers, see From source.

Expose the stack through a Cloudflare Tunnel

Section titled “Expose the stack through a Cloudflare Tunnel”

The tunnel service gives the stack a public HTTPS URL, which atproto OAuth needs to sign in with a real account. It runs cloudflared against Caddy and writes the URL to a shared volume. The happyview service waits up to 30 seconds for that file and exports the URL as PUBLIC_URL.

The tunnel service picks its mode in this order:

Your setupModeHostname
CLOUDFLARE_TUNNEL_TOKEN is setNamed tunnel, routed from the Cloudflare dashboardStable
.cloudflared/ holds a tunnel credentials fileLocally managed tunnel, routed from .envStable
NeitherQuick tunnel; no Cloudflare account neededRandom, and changes on every restart

Leave CLOUDFLARE_TUNNEL_TOKEN blank and .cloudflared/ empty. Find the URL in the logs:

docker
docker compose logs tunnel | grep trycloudflare

Anything that pins the instance’s URL, such as an OAuth client registration or a lexicon’s service entry, has to be updated after each restart.

Create a tunnel in the Cloudflare dashboard and set both variables in .env:

from
CLOUDFLARE_TUNNEL_TOKEN=<token from the Cloudflare dashboard>TUNNEL_HOSTNAME=happyview-dev.example.com

The token starts the tunnel, and TUNNEL_HOSTNAME is the value written to the shared URL file. With the token alone, nothing is written, so the instance waits the full 30 seconds and falls back to PUBLIC_URL in .env.

The tunnel service generates the cloudflared configuration from TUNNEL_HOSTNAME and points it at Caddy, so the routing stays out of the Cloudflare dashboard. The one-time setup needs a Cloudflare account with a domain and cloudflared installed on your machine.

cloudflared
# Authorize cloudflared for your domain (opens a browser).cloudflared tunnel login# Create the tunnel. This writes ~/.cloudflared/<tunnel-id>.json.cloudflared tunnel create happyview-dev# Point a hostname at the tunnel.cloudflared tunnel route dns happyview-dev happyview-dev.example.com# Hand the credentials to the stack.cp ~/.cloudflared/<tunnel-id>.json .cloudflared/

Then set TUNNEL_HOSTNAME=happyview-dev.example.com in .env and leave CLOUDFLARE_TUNNEL_TOKEN blank, since a token takes precedence.

Git ignores everything in .cloudflared/ except its .gitignore. The credentials file runs only that one tunnel (see Cloudflare’s tunnel permissions), but treat it as a secret.

Start only the three local services:

docker
docker compose up happyview web caddy

The instance still waits 30 seconds for a tunnel URL, then uses PUBLIC_URL from .env. To skip the wait, comment out the TUNNEL_URL_FILE line on the happyview service in docker-compose.yml.

Each of these Compose files declares its own project name, so the stacks can run at the same time without sharing containers, networks or volumes.

FileProject nameWhat it runs
docker-compose.ymlhappyviewThe development stack
docker-compose.test.ymlhappyview-testPostgres for the Rust integration tests; see Test
docker-compose.e2e.ymlhappyview-e2eThe full stack with a local PLC directory and PDS, for the browser tests
docker-compose.prod.sqlite.ymlhappyview-prod-sqliteProduction on SQLite; see Docker
docker-compose.prod.postgres.ymlhappyview-prod-postgresProduction on Postgres; see Docker

The other Compose files are docker-compose.e2e.ci.yml, which CI layers over the end-to-end stack, and the four interop stacks listed in Interop tests.

Every commit needs a Signed-off-by line, which certifies the Developer Certificate of Origin. The DCO check is required, so a pull request can’t merge without it.

git
git commit -s -m "fix: handle empty lexicon lists"

If the check fails, sign off the existing commits and force-push:

git
git rebase --signoff upstream/alphagit push --force-with-lease

To avoid rewriting history, push one more signed-off commit instead. Its sign-off covers the earlier commits in the pull request:

git
git commit --allow-empty -s -m "chore: sign off previous commits"git push

Release versions are computed from commit messages (see Release), so messages follow Conventional Commits: fix:, feat:, docs:, test:, ci:, chore: and so on. Mark a breaking change with feat!: or a BREAKING CHANGE: footer.

The pull request template asks what the change does, why, and how to test it. Its checklist covers the base branch, sign-off, cargo fmt and cargo clippy, tests and docs. The template names beta as the base; for a v3 change, the right base is alpha.

Into alpha, open the pull request as a draft while you iterate. CI skips its costly jobs on a draft, and the e2e-tests check fails on purpose with “This pull request is a draft”. When the work is done and passes locally, mark the pull request ready for review, which runs the full suite on the same commit. A pull request into beta runs the full suite on every push.

gh
gh pr create --draft --base alphagh pr ready

A pull request into alpha or beta can merge once its required checks pass and it has one approving review. The approval must come from someone other than whoever pushed last. Test lists which checks are required. Both branches accept merge commits only.

Other things that can appear on a pull request:

  • Checks waiting to start. A first-time contributor’s CI run waits until a maintainer approves it.
  • audit failing. It fails when a new RustSec advisory affects a dependency, which usually comes from the base branch rather than your change. A maintainer handles it, and it isn’t a required check.
  • A ### PR Build comment. When the change touches the server, CI builds container images and Linux binaries, and a bot comments with docker pull ghcr.io/gamesgamesgamesgamesgames/happyview:pr-<number> and the commands that download the binaries.
  • Vercel failing on a fork. It builds a docs preview and fails with “Authorization required to deploy” until a maintainer authorizes it. It isn’t a required check.