Express API server for ingesting sound detection events from Raspberry Pi sensor nodes, storing them in PostgreSQL, and uploading audio recordings to S3. Runs locally or as an AWS Lambda function via serverless-http.
npm installCreate a .env file in the project root, used for local testing:
# Server
PORT=3000
# PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_NAME=train_detection
DB_USER=camille
DB_PASSWORD=your_password_here
DB_SSL=false
# Optional: iMessage alerts (macOS - local testing only)
ALERT_PHONE_NUMBER=+1xxxxxxxxxx
# Detection thresholds (optional, these are the defaults)
TRAIN_MIN_DECIBELS=65
TRAIN_MIN_DURATION_SECONDS=1
# AWS S3 (for audio storage)
AWS_REGION=us-west-2
AWS_S3_BUCKET=your_bucket_nameFor Lambda function, these values are needed as environment variables:
AWS_S3_BUCKET=your-bucket-name
DATABASE_URL=postgresql://neondb_owner...
TRAIN_MIN_DECIBELS=65
TRAIN_MIN_DURATION_SECONDS=1
psql -U camille -d train_detection -f db/schema.sqlnpm startThe server runs on port 3000 by default (PORT env var to override).
See API.md for full endpoint documentation.
This server exports a handler for use with AWS Lambda + API Gateway via serverless-http.
The POST /api/detections endpoint accepts multipart/form-data uploads containing .wav files. API Gateway must be configured to treat multipart/form-data as a binary media type, otherwise the audio data will be corrupted in transit.
Run the deploy script:
npm run deployThis does the following in sequence:
npm install --omit=dev— ensures production dependencies are up to date- Creates a timestamped zip (e.g.
deployment_07_31_2026_14_30_00.zip) from the project root, excluding.git,.env, existing zips, and SQL dump files - Uploads the zip to Lambda via the AWS CLI targeting the
train-detection-expressfunction
After deploying, verify in the Lambda console — the Code tab will show an updated Last modified timestamp. Use the Test tab to run a health check event.
Deployment zips are covered by the
*.zipgitignore pattern and should not be committed.
Local Postgres user is
camille(macOS default — no password required).DB_USER=camillein.env.
Local database:
pg_dump -U camille train_detection > local_dump_MM_DD_YYYY.sqlRemote database (via TRAIN_DATABASE_URL_PROD):
Use --no-owner --no-privileges to strip out hosted-Postgres-specific roles (e.g. Neon's neondb_owner) that don't exist locally:
pg_dump "$TRAIN_DATABASE_URL_PROD" --no-owner --no-privileges > remote_dump_MM_DD_YYYY.sqlTerminate any connections to the db, drop and recreate the local database (to avoid schema conflicts), then restore with local or remote dump:
psql -U camille -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'train_detection' AND pid <> pg_backend_pid();"
dropdb -U camille train_detection
createdb -U camille train_detection
psql -U camille -d train_detection < remote_dump_MM_DD_YYYY.sqlIf the database doesn't exist yet, skip the terminate/drop steps and run
createdb+ restore directly.