Self-hosted URL shortener running on Google Cloud App Engine + Firestore. Users register redirect paths (1kg.me/{group-slug}/{path} → URL). Access is controlled by ownership and group-based permissions.
Stack: Node.js 24, Express 4, Firestore, Google OAuth2 + JWT.
See CHANGELOG.md for the full version history.
- Node.js >= 24
- Google Cloud CLI
- GCP project with App Engine and Firestore (Native mode) enabled
npm install
cp .env.example .env
# Edit .env with your values
gcloud auth application-default login
npm run dev| Variable | Description | Required | Default |
|---|---|---|---|
PORT |
Server port | No | 3000 |
NODE_ENV |
Environment (development/production/test) |
No | — |
CORS |
Comma-separated allowed origins | Yes (production) | * |
JWT_SECRET |
Secret for signing and verifying JWTs (min 32 chars recommended) | Yes | — |
JWT_TTL |
JWT expiry duration (e.g. 2h, 1d, 30m) |
No | 2h |
GOOGLE_CLIENT_ID |
Client ID from Google Cloud Console | Yes | — |
GOOGLE_CLIENT_SECRET |
Client Secret from Google Cloud Console | Yes | — |
GOOGLE_OAUTH_REDIRECT |
OAuth2 callback URL (e.g. https://1kg.me/api/v1/auth/google/callback) |
Yes | — |
JWT_SECRETandGOOGLE_CLIENT_SECRETmust never be written intoapp.yaml. Use Secret Manager or environment injection at deploy time.
Production:
CORSdefaults to*(any origin). Always set it explicitly to the allowed origin(s) (e.g.https://1kg.me) in your production environment or deploy pipeline. Leaving it as*allows any browser origin to make credentialed API requests.
npm test # unit + integration (Jest)
npm run test:e2e # end-to-end (requires running server + real Firestore)Query GCP Cloud Logging for the active project (requires gcloud CLI authenticated):
npm run logs # last 100 ERROR-level entries
npm run logs:all # last 100 entries (all severities)Order matters: Firestore indexes must be deployed and reach READY status before the application is deployed. Deploying the app first will cause 500 errors on the first requests that hit composite-index queries (non-admin users listing redirects or groups).
npm run indexesThis syncs firestore.indexes.json to the active GCP project. Wait until all indexes show READY in the Firestore console before proceeding.
Alternatively, create them manually:
gcloud firestore indexes composite create \
--collection-group=redirects \
--field-config=field-path=owner,order=ASCENDING \
--field-config=field-path=updated,order=DESCENDING
gcloud firestore indexes composite create \
--collection-group=redirects \
--field-config=field-path=permission,array-config=CONTAINS \
--field-config=field-path=updated,order=DESCENDING
gcloud firestore indexes composite create \
--collection-group=groups \
--field-config=field-path=slug,order=ASCENDING \
--field-config=field-path=updated,order=DESCENDING
gcloud firestore indexes composite create \
--collection-group=users \
--field-config=field-path=deletedAt,order=ASCENDING \
--field-config=field-path=updated,order=DESCENDING
gcloud firestore indexes composite create \
--collection-group=groups \
--field-config=field-path=deletedAt,order=ASCENDING \
--field-config=field-path=updated,order=DESCENDINGgcloud app deploy app.yamlSee docs/api/v1.md for full endpoint documentation.