Web app for reserving office parking spots, backed by SpacetimeDB. Built with Qwik and QwikCity.
- Node.js
^18.17.0 || ^20.3.0 || >=21.0.0 - A Google Cloud project with OAuth 2.0 credentials
- Optionally a GitHub OAuth app, to offer GitHub sign-in as well
- SpacetimeDB table
Copy the example file and fill in your credentials:
cp .env.example .env| Variable | Description | Required |
|---|---|---|
GOOGLE_CLIENT_ID |
Google OAuth 2.0 Client ID | Yes |
GOOGLE_CLIENT_SECRET |
Google OAuth 2.0 Client Secret | Yes |
GITHUB_CLIENT_ID |
GitHub OAuth app Client ID | No |
GITHUB_CLIENT_SECRET |
GitHub OAuth app Client Secret | No |
OAUTH_REDIRECT_URI |
Callback URL for both providers, localhost by default | No |
PUBLIC_SPACETIMEDB_MODULE |
Database name | Yes |
PUBLIC_SPACETIMEDB_URI |
Database cluster | Yes |
API_KEYS |
Comma-separated API keys for REST API access | No |
Users sign in with Google or with GitHub; both providers land on the same
/api/auth/callback route and the provider is chosen with /api/auth?provider=github.
Leave the GITHUB_* variables unset and the GitHub button simply will not work.
Sign-in is not an access control — the parking data is public by design. It exists
to deter bots and to prefill the reservation name, so any Google or GitHub account is
accepted. Reservations are keyed by the display name the provider returns (name,
falling back to the @login for GitHub accounts with no name set), so signing in
through a different provider than usual can produce a different name.
├── public/ Static assets
├── src/
│ ├── components/ Shared components
│ ├── routes/ Directory-based routing (pages + API endpoints)
│ ├── services/ Google Sheets & OAuth helpers
│ ├── plugins/ Fastify server plugin
│ ├── entry.ssr.tsx SSR entry point
│ └── entry.fastify.tsx Production server entry point
├── adapters/
│ └── fastify/ Fastify adapter Vite config
└── scripts/ One-off maintenance scripts
Before SpacetimeDB the reservations lived in a Google Spreadsheet with one tab
per year. scripts/migrate-from-sheet.mjs imports a year of that spreadsheet
into the database, treating the spreadsheet as the source of truth — it
overwrites the spots and every reservation on the dates the sheet covers.
node scripts/migrate-from-sheet.mjs --year 2026 # dry run: report only
node scripts/migrate-from-sheet.mjs --year 2026 --apply # write to the database
node scripts/migrate-from-sheet.mjs --help # all flagsRequires the spacetime CLI, logged in
(spacetime login). Run the dry run first — it prints the spot list, date range
and reservation count it parsed, plus any rows it could not make sense of.
What it does not touch: reservations on dates outside the sheet (pass
--wipe to clear those too), and spots the sheet no longer lists (pass
--remove-missing-spots to delete those and all of their reservations).
The reducers the script calls are restricted to the identities listed in
ADMIN_IDENTITIES in spacetimedb/src/index.ts — reducers are callable by any
client that can reach the module, so a "delete every reservation" reducer cannot
be left open. If a call is rejected, the error names the calling identity; add it
there and re-publish. These reducers exist only for this import and can be
deleted once it is done.
npm install
npm startThe dev server runs at http://localhost:5173 with SSR and hot module replacement.
npm run buildThis generates dist/ (client assets) and server/ (Fastify server bundle). To run it locally:
ORIGIN=http://localhost:3000 npm run serveThe ORIGIN environment variable is required for CSRF protection and must match the URL users access the app from.
docker build -t nice-parking .
docker run -p 3000:3000 --env-file .env -e ORIGIN=https://your-domain.com nice-parking# Start in the background
docker compose up -d
# With a custom origin
ORIGIN=https://your-domain.com docker compose up -d
# View logs
docker compose logs -f
# Stop
docker compose downThe compose file reads credentials from your .env file and defaults ORIGIN to http://localhost:3000 if not set.
A REST API (/api/v1) is available for AI agent and programmatic access. Authentication is via Bearer token — set the API_KEYS environment variable with one or more comma-separated keys.
- AI agent guidance:
/llms.txt— plain-language overview and workflow tips - OpenAPI spec:
/openapi.json— machine-readable specification for tool generation
- Set
ORIGINto your actual domain (e.g.https://parking.example.com) -- required for CSRF protection - Set
OAUTH_REDIRECT_URIto your production callback URL (https://your-domain.com/api/auth/callback) — it defaults to the localhost one, so a deployment that leaves it unset will bounce users tolocalhost:5173 - Register that same URL in your Google Cloud Console and as the Authorization callback URL of your GitHub OAuth app
- Ensure outbound HTTPS access to
accounts.google.com,googleapis.com,github.comandapi.github.com