- Node.js 18+
- Access to a MongoDB deployment (Atlas or self-hosted)
- Firebase project with service account credentials that can access Cloud Storage and the Realtime Database
Create a .env file (or export the variables in your shell) with the following keys:
| Variable | Description |
|---|---|
MONGODB_URI |
MongoDB connection string |
DATABASE_NAME |
MongoDB database name |
COLLECTION_NAME |
Collection that stores survey documents |
FIREBASE_STORAGE_BUCKET |
Firebase Storage bucket used for export archives and status files (e.g. your-project.appspot.com) |
GOOGLE_APPLICATION_CREDENTIALS (optional) |
Absolute path to your Firebase service-account JSON file |
API_KEYS (optional) |
Comma-separated list of shared secrets required in the x-api-key header. When omitted, the API key guard is disabled. |
API_KEY_HEADER (optional) |
Override the header name checked for API keys. Defaults to x-api-key. |
RATE_LIMIT_WINDOW_MS (optional) |
Request throttling window in milliseconds. Defaults to 900000 (15 minutes). |
RATE_LIMIT_MAX (optional) |
Maximum number of requests per IP within the window. Defaults to 100. |
TRUST_PROXY (optional) |
Set to true when deploying behind a reverse proxy/load balancer so rate limiting uses the forwarded client IP. |
- In the Firebase console, create a service account key with access to Cloud Storage (read/write) for the project bucket.
- Download the JSON key file and place it in the project root as
GOOGLE_APPLICATION_CREDENTIALS.json. - You can optionally set
GOOGLE_APPLICATION_CREDENTIALS(orFIREBASE_CREDENTIALS_PATH) to the absolute path of the JSON file. If unset, the app automatically falls back to the rootGOOGLE_APPLICATION_CREDENTIALS.jsonfile when it exists. - When running in deployed environments, configure the same variables via your host's secret manager or environment configuration.
- Export runs are asynchronous: the API responds immediately with a
jobIdand a status endpoint. - Job metadata is stored next to the archive as
exports/{jobId}.jsonin the configured Storage bucket. - Poll
GET /api/export/status/{jobId}. The server reads the companion JSON file and, when the job is complete, includes a short-lived signed URL forexports/{jobId}.zip. - Each archive contains
metadata.jsonwith survey summaries, counts, and timing details alongside the requested data file:data.jsonfor the default export ordata.csvwhen the CSV format is selected. The CSV export produces a flattened table and adds dynamic columns for new keys detected across observations. - If you cancel or delete an export, remove both the
.zipand.jsonfiles from Storage to clean up.
- Rate limiting: Every request passes through
express-rate-limitwith sane defaults (100 requests per 15 minutes). Tune viaRATE_LIMIT_WINDOW_MSandRATE_LIMIT_MAX. - API keys: Provide a comma-separated list in
API_KEYSand supply one of the values in thex-api-keyheader on each request. Change the header name withAPI_KEY_HEADER. IfAPI_KEYSis blank, the guard stays disabled—for staging environments, use separate keys or IP allow lists as needed. - Reverse proxies: When deploying behind a load balancer (e.g., Cloud Run, App Engine, Nginx), set
TRUST_PROXY=trueso rate limiting measures the originating client IP instead of the proxy. - Health checks: The
/healthendpoint remains unauthenticated for platform probes. Adjust the exempt route list insrc/middleware/security.jsif you need stricter controls.
- Purpose: Lightweight readiness probe.
- Authentication: Not required.
- Response:
{
"status": "ok"
}- Purpose: Read-only pass-through to MongoDB for UI search/browse flows.
- Authentication: API key header required when
API_KEYSis configured. - Allowed actions:
find,findOne,aggregate. - Request body (example: typeahead by survey name):
{
"pipeline": [
{ "match": { "surveyName": { "$regex": "^air", "$options": "i" } } },
{ "group": { "_id": "$surveyName", "surveyIds": { "$addToSet": "$_id" }, "collections": { "$addToSet": "$collections" }, "startTime": { "$min": "$startTime" }, "stopTime": { "$max": "$stopTime" } } },
{ "project": { "_id": 0, "surveyName": "$_id", "surveyIds": 1, "collections": 1, "startTime": 1, "stopTime": 1 } },
{ "sort": { "surveyName": 1 } },
{ "limit": 20 }
]
}- Response:
{
"documents": [
{
"surveyName": "Air Quality",
"surveyIds": ["66eab5a9f1..."],
"collections": [["Field Notes", "Photos"]],
"startTime": 1704067200000,
"stopTime": 1704499200000
}
]
}ℹ️ The proxy automatically converts
_idstrings intoObjectIds and blocks non-read actions.
- Purpose: Kick off asynchronous export jobs for the selected surveys.
- Authentication: API key header required when enabled.
- Request body:
{
"surveyIds": ["66eab5a9f17d4c31f5e7a4b2", "66eab5a9f17d4c31f5e7a4b3"],
"includeMedia": true,
"format": "csv"
}-
includeMedia(optional, defaultfalse): whentrue, Firebase media URLs referenced in the observations are downloaded into amedia/folder within the archive and sibling*_pathfields point to those files. -
format(optional, defaultjson): set tocsvto receive a flatteneddata.csvinstead ofdata.json. Leave asjsonto keep the JSON file. -
Response:
{
"jobId": "1c2b3a4d-5678-90ef-1234-abcdef012345",
"status": "processing",
"statusCheckEndpoint": "/api/export/status/1c2b3a4d-5678-90ef-1234-abcdef012345"
}- Purpose: Poll the job state and collect the download URL when complete.
- Authentication: API key header required when enabled.
- Response (complete job):
{
"jobId": "1c2b3a4d-5678-90ef-1234-abcdef012345",
"status": "complete",
"progress": 100,
"displayName": "Air-Quality-2024-09-29.zip",
"fileSize": 1234567,
"observationCount": 245,
"aggregatedSurveyCount": 2,
"downloadUrl": "https://storage.googleapis.com/..."
}- Response (error):
{
"jobId": "1c2b3a4d-5678-90ef-1234-abcdef012345",
"status": "error",
"error": "Export failed: <details>"
}npm installnpm run devnpm test- The app uses a lightweight logger that defaults to
debuglevel in development andinfoin production. - Override the level by setting
LOG_LEVELto one ofdebug,info,warn, orerror. - Example:
LOG_LEVEL=debug npm run devenables the verbose messages that help trace media exports locally while keeping production output tidy.