- Prerequisites
- Frontend setup
- Backend setup
- Docker Compose setup
- Google OAuth setup
- Environment reference
- Node.js 22.12+
- npm 10+
- Java 25
- Docker + Docker Compose
- A Google Cloud project if you need real OAuth login
Important repo note:
- there is no root
package.json, so npm commands run fromapps/web; apps/web/package.jsondeclares the frontend Node runtime contract, and GitHub Actions verifies it with Node 22.12.0;- backend configuration comes from shell environment or Docker Compose, not from an auto-loaded
apps/backend/.env.
Install dependencies and start Vite:
cd apps/web
npm -v
node -v
npm installUse Node.js 22.12.0 or newer before running the frontend commands above. That matches the current Docker build image and the Vite 8 baseline this repository is moving toward.
Create apps/web/.env.local when you want explicit local overrides:
VITE_API_BASE_URL=http://localhost:3000
VITE_DEFAULT_USER_ID=demo-userRun the frontend:
cd apps/web
npm run devUseful frontend commands:
cd apps/web
npm run lint
npm run test
npm run build
npm run checkStart a local Postgres instance first. The easiest repo-native path is the Compose database profile:
docker compose --profile db up -d dbIf port 5432 is already occupied, keep DB_PORT=5432 for container-to-container
traffic and choose a free host port, for example DB_HOST_PORT=55432.
For the standard local setup, the backend dev profile now provides matching defaults automatically:
DB_HOST=localhostDB_PORT=5432DB_NAME=habbit_runnerDB_USER=habbitDB_PASSWORD=passwordDB_SCHEMA=habbit_runnerAPI_PORT=3000AUTH_SECRET=change-me(development profile only)ACCESS_TOKEN_TTL_SECONDS=3600REFRESH_TOKEN_EXPIRES_DAYS=30JWT_ISSUER=habittracker-localAPI_PUBLIC_URL=http://localhost:3000OAUTH_DEFAULT_RETURN_TO=http://localhost:5173CORS_ORIGINS=http://localhost:5173
DB/auth secrets should come from your shell environment or from a local env file that you source before starting Quarkus. The dev profile still has fallback values for convenience, but production startup and readiness reject placeholder auth secrets and localhost callback URLs.
Repo helper for that flow:
cd apps/web
npm run dev:serverThat helper sources the workspace root .env when it exists, overrides the host-dev callback/origin values back to 3000 and 5173, and on macOS tries Java 25 via java_home with an SDKMAN fallback.
Only export variables when you need to override those defaults or enable optional integrations:
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
export VAPID_SUBJECT=mailto:admin@localhostThen run Quarkus dev mode:
cd apps/backend
./mvnw clean quarkus:devUseful backend commands:
cd apps/backend
./mvnw test
./mvnw package -DskipTestsNotes:
- Flyway migrations run automatically on backend startup.
- the Compose
dbprofile now publishes PostgreSQL onlocalhost:5432for host-based Quarkus runs; JAVA_HOMEshould point to Java 25 before starting the backend;- If OAuth is not configured, UI-only work can still proceed with sync disabled or with limited unauthenticated screens.
Copy the root env template:
cp .env.example .envThe template contains local-only database and auth values. Replace both before using the stack in any shared or production environment. Compose refuses to resolve when either required value is blank or missing.
For the full local stack with the bundled database:
docker compose -f docker-compose.yml -f docker-compose.local.yml --profile db up --buildFor a stack that targets an already running external database:
docker compose up --buildUseful commands:
docker compose --profile db up -d db
docker compose logs -f api
docker compose logs -f web
docker compose down -vCurrent Compose behavior:
dbis behind thedbprofile;apiis exposed only to the internal Docker network;- browser traffic reaches the backend through the
webnginx proxy at/api; docker-compose.local.ymlexposes the web app onhttp://localhost:5137.- health probes are available through
/api/q/health/liveand/api/q/health/readyon the web ingress; the API container also checks readiness internally; - metrics are outbound New Relic telemetry, not a public
/metricsroute; docker-compose.dokploy.ymlis the Dokploy entry point. It keeps the API without a host port, exposes web port 80 to the configured router, and attaches both services to the externaldokploy-ipv6network.- When
VAPID_PUBLIC_KEYis empty,/q/health/readystays UP and the notification check reportsstatus=disabledinstead of blocking startup.
Dokploy deployment:
docker compose -f docker-compose.dokploy.yml config --quiet
docker compose -f docker-compose.dokploy.yml up -d --build --remove-orphansThe local health checks above do not prove deployed ingress. After a Dokploy rollout, verify the actual configured domain separately with a body-suppressing request such as:
curl -fsS -o /dev/null -w 'deployed ready HTTP %{http_code}\n' \
https://<configured-domain>/api/q/health/readyDo not add credentials or metrics payloads to these checks or to shell history.
- In Google Cloud Console, configure an OAuth consent screen with
email,profile, andopenid. - Create a Web application client.
- Add these redirect URIs as needed:
- local backend:
http://localhost:3000/auth/google/callback - local preview proxy:
http://localhost:5137/api/auth/google/callback - docker through nginx:
https://yourdomain.com/api/auth/google/callback
- local backend:
- Set:
GOOGLE_OAUTH_CLIENT_IDGOOGLE_OAUTH_CLIENT_SECRETAPI_PUBLIC_URLOAUTH_DEFAULT_RETURN_TO
- Restart the backend after changing auth configuration.
For this repository's host-based dev flow, the callback URI must match API_PUBLIC_URL. If your root .env points local auth through the preview proxy, use http://localhost:5137/api/auth/google/callback; if you talk to the backend directly, use http://localhost:3000/auth/google/callback.
Use matching origins:
- local frontend:
http://localhost:5173 - docker default local web origin:
http://localhostorhttp://localhost:5137depending on which compose file you use
- In @BotFather, create or select the production
bot and use Bot Settings → Mini Apps → Configure Mini App to set the
Main Mini App URL to the deployed HTTPS web origin (for example,
https://habit-runner.freeddns.org). Telegram must be able to reach this URL over HTTPS; localhost is for local browser testing only. - Keep local, staging, and production bots/URLs separate. A test account must launch the staging Mini App, sign in with Telegram, link an email account in both directions, close/reopen the webview, and confirm the same habits and check-ins are visible before production rollout.
- Provision
TELEGRAM_BOT_TOKENonly in the backend runtime secret store. Never put it inVITE_*, the static web image, source control, logs, or a client request. SetTELEGRAM_INIT_DATA_MAX_AGE_SECONDSto the freshness bound accepted by the deployment (the default is 86400 seconds). - Set the public
VITE_TELEGRAM_BOT_USERNAMEbuild variable to the bot username (without a required@). The account page uses it to open at.medeep link; the Main Mini App URL itself remains the deployed HTTPS website root configured in BotFather.
Telegram deep links use https://t.me/<bot_username>?startapp=<pairing-code>
for short-lived account-link challenges. The account owner must still confirm
the link, and the payload must never contain the bot token.
After changing BotFather settings or runtime secrets, restart the backend and
check /api/q/health/ready before manual testing.
| Variable | Required | Purpose |
|---|---|---|
WEB_PORT |
Yes | Host port mapped to the web container (default: 5137) |
API_PORT |
Yes | Backend HTTP port inside the container |
DB_HOST |
Yes | Database host for the backend |
DB_PORT |
Yes | Database connection port used by the backend |
DB_HOST_PORT |
No | Host port published by the bundled Compose database; defaults to DB_PORT for backward compatibility |
DB_NAME |
Yes | Database name |
DB_USER |
Yes | Database user |
DB_PASSWORD |
Yes | Database password |
DB_SCHEMA |
Yes | Active schema for Flyway and Hibernate |
AUTH_SECRET |
Yes | JWT signing secret |
ACCESS_TOKEN_EXPIRES_IN |
No | Human-readable token TTL |
ACCESS_TOKEN_TTL_SECONDS |
Yes | Access token TTL in seconds |
REFRESH_TOKEN_EXPIRES_DAYS |
Yes | Refresh token lifetime |
JWT_ISSUER |
Yes | JWT issuer |
API_PUBLIC_URL |
Yes | Public backend URL used in OAuth redirects |
OAUTH_DEFAULT_RETURN_TO |
Yes | Frontend URL after auth |
CORS_ORIGINS |
Yes | Allowed frontend origins |
NEW_RELIC_METRICS_ENABLED |
No | Enables New Relic metric export; requires NEW_RELIC_LICENSE_KEY when true |
GOOGLE_OAUTH_CLIENT_ID |
Optional | Needed for real Google sign-in; required for production startup and readiness |
GOOGLE_OAUTH_CLIENT_SECRET |
Optional | Needed for real Google sign-in; required for production startup and readiness |
TELEGRAM_BOT_TOKEN |
Optional locally; required in production | Backend-only bot token used to verify Telegram Mini App init data |
TELEGRAM_INIT_DATA_MAX_AGE_SECONDS |
No | Maximum Telegram init-data age; defaults to 86400 seconds |
VITE_TELEGRAM_BOT_USERNAME |
No | Public bot username for website-to-Telegram Mini App deep links |
VAPID_PUBLIC_KEY |
Optional | Browser push public key |
VAPID_PRIVATE_KEY |
Optional | Browser push private key |
VAPID_SUBJECT |
Optional | Web Push contact |
| Variable | Default | Purpose |
|---|---|---|
VITE_API_BASE_URL |
http://localhost:3000 in dev |
API origin override |
VITE_DEFAULT_USER_ID |
demo-user |
Demo/default user seed |