- TypeScript 80.4%
- CSS 15.1%
- Go Template 1.9%
- Dockerfile 1.8%
- HTML 0.8%
| .forgejo/workflows | ||
| charts/app | ||
| design | ||
| src | ||
| web | ||
| wiki | ||
| .gitignore | ||
| biome.json | ||
| bun.lock | ||
| Dockerfile | ||
| MIGRATION.md | ||
| mobile-top.png | ||
| package.json | ||
| README | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
hello-world
Hello World!
That one line is the entire history of this repo — it was migrated from
octocat/Hello-World, GitHub's original
demo repository, and the original README is still here, untouched.
This app gives that line some company: a wall of hellos. Twelve greetings ship with it; anyone signed in can add theirs and remove their own.
Live: https://hello-world-imp-e2e-org.open-platform.sh
What it does
- The wall — every greeting, newest first, as cards showing the greeting, its language, who posted it, and when.
- Filter — type in the search box to narrow the wall by language, text or
author. Instant, client-side;
GET /api/greetings?q=does the same thing for API consumers. - Post a greeting — the composer takes a language and a greeting. Where the deployment has a sign-in, posting requires it and the greeting is attributed to your account; where it has none, the wall takes greetings from anyone, with an optional "From" signature (see Auth).
- Remove your own — the Remove button only appears on greetings you posted, and the API enforces that independently (403 otherwise).
Stack
| Layer | Choice |
|---|---|
| Server | Fastify 5 + TypeScript (src/), TypeBox schemas → generated OpenAPI |
| UI | React 19 SPA (web/), component kit in web/src/ui/ |
| Data | Postgres (CNPG), migrations in src/migrate.ts |
| Styling | Org design tokens only, served at /tokens.css |
| Lint | biome |
| Runtime | Bun on oven/bun:1-alpine (see Dockerfile) |
Endpoints
| Method | Path | Auth | What |
|---|---|---|---|
GET |
/ |
public | The SPA |
GET |
/version |
public | { name, version, store } |
GET |
/readyz |
public | Readiness — is the store answering? |
GET |
/api/me |
public | Who is asking (anonymous → null) |
GET |
/api/greetings |
public | The wall (?q= filters) |
POST |
/api/greetings |
see below | Add a greeting |
DELETE |
/api/greetings/:id |
session | Remove your own greeting |
GET |
/openapi.json |
public | OpenAPI 3.1, generated from the routes |
GET |
/tokens.css |
public | Design token fallback stylesheet |
Auth
The app never assumes an edge is gating it: it resolves identity itself, on
every request, in a preValidation hook that runs before a body is parsed.
A wall of hellos holds nothing private, so reads are public everywhere. Writes follow the identity the deployment actually has:
| Deployment | Posting | Removing |
|---|---|---|
In-app sign-in exists (mode: app) |
Requires a session; attributed to your account | Your own greetings |
Behind the forwardAuth edge (mode: platform) |
Every request is already authenticated → attributed to it | Your own greetings |
No identity source (mode: none) |
Open, signed with an optional "From" name, rate-limited per IP | Nobody |
That last row is a deliberate choice: this deployment has no identity source (the platform's forwardAuth edge is off org-wide and no in-app OAuth credentials are provisioned), and a wall nobody can ever write to is an ornament. Nothing on the wall is private, so opening posting there costs no confidentiality — and the moment a sign-in exists, the app requires it again with no code change. Deleting is always identity-bound and checked server-side.
Identity comes from whichever source the deployment configures (src/config.ts,
authMode()):
platform(PLAT_FORWARD_AUTH=1) — the platform edge authenticates the request and injects verifiedX-Plat-*headers. WhenPLAT_EDGE_TOKENis configured, the app additionally demands that shared secret before trusting any header, so hand-crafted headers sent straight at the pod fail closed.app(FORGEJO_BASE+FORGEJO_CLIENT_ID+FORGEJO_CLIENT_SECRET) — in-app Forgejo SSO, OAuth2 authorization-code flow, session in an HMAC-signed httpOnly cookie (BETTER_AUTH_SECRETsigns it).none— no identity source configured: everyone is anonymous, so the wall accepts signed-but-unauthenticated greetings (10 per IP per 10 minutes) and the UI says so instead of offering a sign-in that cannot work.
Data
One table, greetings, created and seeded by numbered migrations in
src/migrate.ts (see wiki/Data-Model.md). Migrations run at boot, before the
pod reports ready, and are also runnable standalone (npm run migrate /
bun dist/migrate.js). To change the schema, append a migration — never edit an
applied one.
When no database is configured (local dev, or a deployment provisioned without
one) the app falls back to a JSON file on its mounted volume rather than
refusing to boot, and /readyz reports which store answered — degraded: true
if Postgres was configured but unreachable.
Development
bun install
npm run build # tsc → dist/, vite → web/dist/
npm start # serve on :8080 (DATA_DIR=/tmp for the file store)
npm run lint # biome
npm run typecheck # server + web
npm run lint, npm run typecheck and npm run build all have to pass before
anything is pushed; CI (.forgejo/workflows/check.yml) additionally builds the
container image from the Dockerfile.
Design tokens
The UI is styled only with the org's design vocabulary — var(--color-…),
var(--space-…), var(--radius-…) and friends. design/tokens.default.json
is this app's fallback copy of that vocabulary, compiled to /tokens.css at
boot; when the org runs a design app, PLAT_DESIGN_URL makes its tokens.css
load afterwards so org values win and a dead design app degrades to the
defaults instead of to an unstyled page.
Platform plumbing
Dockerfile, charts/app/ and .forgejo/workflows/ come from the platform and
are left alone. The chart mounts a volume at /data, probes /readyz for
startup and readiness and /version for liveness, and injects PORT,
APP_NAME, PLAT_* and the per-app database/auth secrets.