Retro Swedish alphabet time trial built with Next.js 16. Players enter a username, type abcdefghijklmnopqrstuvwxyzåäö as fast as possible, and compete on a persistent leaderboard where each username keeps only its best time.
- Retro single-screen race UI using the provided wordmark and square logo.
- Best-time-per-username storage using Azure Cosmos DB when configured.
- Live leaderboard fan-out through Azure Web PubSub.
- Admin wipe console at
/adminprotected by a shared secret. - Local in-memory fallback for development when Azure credentials are not configured.
Install dependencies and start the app:
npm install
npm run devOpen http://localhost:3000.
Create .env.local with the values you want to use:
AZURE_COSMOS_CONNECTION_STRING="AccountEndpoint=https://...;AccountKey=...;"
AZURE_COSMOS_DATABASE_NAME="alfabetsrace"
AZURE_COSMOS_HIGHSCORES_CONTAINER="highscores"
AZURE_WEB_PUBSUB_CONNECTION_STRING="Endpoint=https://...;AccessKey=...;Version=1.0;"
NEXT_PUBLIC_WEB_PUBSUB_HUB_NAME="highscores"
ADMIN_SECRET="replace-with-a-long-random-secret"Notes:
- If
AZURE_COSMOS_CONNECTION_STRINGis missing, the app stores highscores in memory and resets on restart. - If
AZURE_WEB_PUBSUB_CONNECTION_STRINGis missing, the app still works but live multi-client leaderboard updates are disabled. ADMIN_SECRETis required for the wipe endpoint and/adminworkflow.
Minimum services for the intended production setup:
- Azure Cosmos DB for NoSQL.
- Azure Web PubSub.
- Azure Static Web Apps with hybrid Next.js hosting, or Azure App Service if you want a more mature runtime target.
Recommended Cosmos settings:
- Database:
alfabetsrace - Container:
highscores - Partition key:
/normalizedUsername
The app creates the database and container on demand if the configured account allows it.
Visit /admin, enter the shared secret, and type WIPE HIGHSCORES exactly. The app deletes all leaderboard records and broadcasts a reset event to connected clients.
Run these checks before deploying:
npm run lint
npm run buildManual checks:
- Complete a race with a new username and confirm it appears on the leaderboard.
- Repeat with the same username and a slower time; confirm the best time does not worsen.
- Repeat with the same username and a faster time; confirm the existing row updates.
- Open two browser sessions with Azure Web PubSub configured and confirm a new score appears live in both.
- Type a wrong character during a race, confirm the timer keeps running, backspace the bad tail, and finish successfully.
Azure Static Web Apps hybrid Next.js supports App Router and Route Handlers, which is enough for this app. Realtime transport is delegated to Azure Web PubSub, so the app runtime does not need to host raw WebSocket sessions directly.
If Static Web Apps preview constraints become a problem, move the same app to Azure App Service and keep the rest of the architecture unchanged.
The repository includes Bicep templates in infra/ for a low-cost Azure setup:
- Cosmos DB for NoSQL in serverless mode.
- Web PubSub in Free tier (
Free_F1). - Static Web Apps in Free tier.
- Azure CLI installed and authenticated.
- Access to an Azure subscription.
az login
az account set --subscription "<subscription-id-or-name>"Create the resource group:
az group create --name alfabetsrace-prod-rg --location westeurope --tags "Creation date=2026-04-10" "Keep until=2027-04-10" "Responsible email=erik.rundberg@omegapoint.se"Validate infrastructure:
az deployment group validate \
--resource-group alfabetsrace-prod-rg \
--template-file infra/main.bicep \
--parameters infra/parameters/prod.bicepparamDeploy infrastructure:
az deployment group create \
--name infra-prod-$(date +%Y%m%d%H%M%S) \
--resource-group alfabetsrace-prod-rg \
--template-file infra/main.bicep \
--parameters infra/parameters/prod.bicepparamRead deployment outputs (resource names, hostnames):
az deployment group show \
--resource-group alfabetsrace-prod-rg \
--name <deployment-name> \
--query properties.outputsWorkflow: .github/workflows/infra-deploy.yml
Required repository secret:
AZURE_CREDENTIALS: Service principal JSON forazure/login.
Run from GitHub Actions (workflow is fixed to prod).
Workflow: .github/workflows/app-deploy.yml
Required repository secrets:
AZURE_STATIC_WEB_APPS_API_TOKENAZURE_COSMOS_CONNECTION_STRINGAZURE_WEB_PUBSUB_CONNECTION_STRINGADMIN_SECRET
Optional repository variables (defaults exist in workflow):
AZURE_COSMOS_DATABASE_NAME(defaultalfabetsrace)AZURE_COSMOS_HIGHSCORES_CONTAINER(defaulthighscores)NEXT_PUBLIC_WEB_PUBSUB_HUB_NAME(defaulthighscores)
The app deployment workflow runs on push to main.
For hybrid Next.js, environment variables passed in GitHub Actions are used during build, but SSR/API runtime settings are read from the Static Web Apps resource.
Set these in Azure Portal under Static Web App -> Environment variables (Production), or with Azure CLI:
az staticwebapp appsettings set \
--name <static-web-app-name> \
--resource-group alfabetsrace-prod-rg \
--setting-names \
AZURE_COSMOS_CONNECTION_STRING="<cosmos-connection-string>" \
AZURE_COSMOS_DATABASE_NAME="alfabetsrace" \
AZURE_COSMOS_HIGHSCORES_CONTAINER="highscores" \
AZURE_WEB_PUBSUB_CONNECTION_STRING="<webpubsub-connection-string>" \
NEXT_PUBLIC_WEB_PUBSUB_HUB_NAME="highscores" \
ADMIN_SECRET="<admin-secret>"After updating app settings, trigger a new deployment or restart the app so runtime picks up the new values.
After infrastructure deployment, get runtime secrets with Azure CLI and add them to GitHub repository secrets.
Get Cosmos DB connection string:
az cosmosdb keys list \
--resource-group alfabetsrace-prod-rg \
--name alfrace-prod-cosmos-ljwqk3vakfmme \
--type connection-strings \
--query "connectionStrings[0].connectionString" \
-o tsvGet Web PubSub connection string:
az webpubsub key show \
--resource-group alfabetsrace-prod-rg \
--name alfrace-prod-wps-ljwqk3vakfmme \
--query primaryConnectionString \
-o tsvGet Static Web Apps deployment token:
az staticwebapp secrets list \
--resource-group alfabetsrace-prod-rg \
--name <static-web-app-name> \
--query properties.apiKey \
-o tsvSet these GitHub repository secrets:
AZURE_COSMOS_CONNECTION_STRINGAZURE_WEB_PUBSUB_CONNECTION_STRINGAZURE_STATIC_WEB_APPS_API_TOKEN
Also generate and set:
ADMIN_SECRETwith a long random value.
Example generator:
openssl rand -hex 32