Nexus API
Multi-channel notification middleware for push, SMS, and email — built on FastAPI and GCP.
🔌 Provider abstraction
Swap or add providers (OneSignal, Twilio, SendGrid) with zero code changes in your app.
📋 Full audit log
Every notification is persisted with status queued → sent | failed, queryable by message_log_id.
⚡ Async delivery
Cloud Tasks decouples API response from provider latency. Clients never block on slow upstream calls.
🔑 API key auth
SHA-256 hashed keys with per-key rate limiting. Managed directly in the database.
Overview
Nexus sits between your FlutterFlow app and messaging providers like OneSignal. Instead of calling OneSignal directly from the client, all notifications flow through Nexus — which handles authentication, rate limiting, async delivery, and a full audit log.
Every send call returns 202 Accepted immediately. Delivery happens asynchronously via GCP Cloud Tasks, with status tracked in PostgreSQL.
How it works
Every send request follows the same eight-step async flow.
POST /push/send (or /sms/send, /email/send) with an X-API-Key header.api_key table. Returns 401 if missing, 403 if invalid or inactive.429 with Retry-After if exceeded.status = queued and a unique id.POST /tasks/{channel} with message_log_id and payload.message_log_id, provider, and status: "queued".POST /tasks/{channel}. The X-CloudTasks-Token shared secret is verified.sent or failed.Local dev: When CLOUD_TASKS_EMULATOR=true, steps 5–7 are bypassed. The worker is called directly via httpx for end-to-end local testing.
Architecture
Organized by communication channel. Each channel has its own router, service, schema, and provider directory.
nexus-api/
├── app/
│ ├── main.py # FastAPI app, middleware, routers
│ ├── config.py # Pydantic BaseSettings
│ ├── dependencies.py # Auth, rate limiter
│ ├── channels/
│ │ ├── push/
│ │ │ ├── router.py # POST /push/send, /push/batch
│ │ │ ├── schemas.py # PushRequest, PushResponse
│ │ │ ├── service.py # Business logic + DB logging
│ │ │ └── providers/
│ │ │ ├── base.py # BasePushProvider ABC
│ │ │ └── onesignal.py # OneSignal implementation
│ │ ├── sms/ # Same structure
│ │ └── email/ # Same structure
│ ├── tasks/
│ │ ├── router.py # POST /tasks/{push,sms,email}
│ │ ├── worker.py # CHANNEL_HANDLERS registry
│ │ └── enqueue.py # Cloud Tasks / httpx direct
│ ├── db/
│ │ ├── session.py # AsyncSession factory
│ │ └── models/
│ │ ├── api_key.py # APIKey ORM model
│ │ └── message_log.py # MessageLog ORM model
│ └── core/
│ ├── logging.py # Structured JSON logging
│ ├── middleware.py # Request ID injection
│ ├── exceptions.py # Global exception handlers
│ └── rate_limiter.py # Token-bucket rate limiter
├── alembic/ # DB migrations
├── terraform/ # GCP infrastructure as code
├── docker-compose.yml
└── Dockerfile # linux/amd64 production image
Database models
api_key
| Column | Type | Description |
|---|---|---|
id | integer | Primary key |
name | varchar | Human-readable label |
key_hash | varchar | SHA-256 hash of the raw API key |
is_active | boolean | Set false to revoke instantly |
rate_limit_per_minute | integer | Max requests/min for this key |
created_at | timestamptz | Creation timestamp |
message_log
| Column | Type | Description |
|---|---|---|
id | integer | Primary key, returned as message_log_id |
channel | enum | push | sms | email |
provider | varchar | e.g. onesignal |
status | enum | queued → sent | failed |
recipients | jsonb | Array of recipient IDs |
payload | jsonb | Full request payload |
error_message | text | Provider error detail, if any |
created_at | timestamptz | Request received timestamp |
updated_at | timestamptz | Last status update |
Authentication
All endpoints except GET /health require an API key in the X-API-Key request header.
| Header | Value | Required |
|---|---|---|
X-API-Key | string | Required |
Keys are stored as SHA-256 hashes in the api_key table. Each key has an independent rate_limit_per_minute and is_active flag for instant revocation.
Auth error responses
| Status | Code | Cause |
|---|---|---|
| 401 | UNAUTHORIZED | X-API-Key header missing entirely |
| 403 | FORBIDDEN | Key found but invalid, revoked, or is_active = false |
| 429 | RATE_LIMIT_EXCEEDED | Key exceeded per-minute quota — check Retry-After |
Example
curl -X POST https://dev-api.myratecards.com/push/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipients":["user-id"],"title":"Hi","body":"Hello"}'
Base URLs
Use the appropriate base URL for each environment.
| Environment | Base URL | Notes |
|---|---|---|
| Local | http://localhost:8000 | Docker Compose · Mac |
| Dev | https://dev-api.myratecards.com | GCP Cloud Run · us-central1 legacy: nexus-api-ttrqossa7a-uc.a.run.app |
| Staging | https://stg-api.myratecards.com | Coming soon |
| Production | https://api.myratecards.com | Coming soon |
Swagger UI at [base_url]/docs · ReDoc at [base_url]/redoc · OpenAPI JSON at [base_url]/openapi.json
Making your first request
Send a push notification in under a minute.
1. Check health
curl https://dev-api.myratecards.com/health
{"status":"ok","version":"0.1.0","environment":"dev"}
2. Send a push notification
curl -X POST https://dev-api.myratecards.com/push/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": ["YOUR_ONESIGNAL_EXTERNAL_ID"],
"title": "Hello from Nexus",
"body": "Your first notification!"
}'
{
"success": true,
"data": { "message_log_id": 1, "provider": "onesignal", "status": "queued" },
"request_id": "13ab78ee-d790-4bf7-9237-39d2278d8fce"
}
Tip: The recipient is the user's OneSignal External ID which equals their Firebase UID after OneSignal.login(uid) is called in the app.
Response format
All API responses use a consistent envelope regardless of endpoint.
Success envelope
{ "success": true, "data": { ... }, "request_id": "uuid-v4" }
| Field | Type | Description |
|---|---|---|
success | boolean | Always true |
data | object | Endpoint-specific payload |
request_id | string | UUID for tracing in Cloud Logging |
Error envelope
{ "success": false, "error": { "code": "ERROR_CODE", "message": "..." }, "request_id": "uuid-v4" }
| Field | Type | Description |
|---|---|---|
success | boolean | Always false |
error.code | string | Machine-readable error code |
error.message | string | Human-readable description |
request_id | string | Use this to look up stack traces in Cloud Logging |
Error codes
Use error.code for programmatic handling. Use request_id to look up stack traces in Cloud Logging.
| HTTP | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED | X-API-Key header missing entirely. |
| 403 | FORBIDDEN | Key found but invalid, revoked, or is_active = false. |
| 422 | VALIDATION_ERROR | Request body failed Pydantic validation. Check error.message for field details. |
| 429 | RATE_LIMIT_EXCEEDED | Exceeded per-minute quota. Check the Retry-After response header. |
| 401 | TASK_AUTH_FAILED | X-CloudTasks-Token missing or wrong on a /tasks/* endpoint. |
| 502 | PROVIDER_ERROR | Upstream provider (OneSignal etc.) returned an error. Check error.message. |
| 500 | INTERNAL_ERROR | Unexpected server error. Use request_id to search Cloud Logging. |
Example
{
"success": false,
"error": { "code": "FORBIDDEN", "message": "Invalid or inactive API key." },
"request_id": "c8381f83-7fc-4abd-ae86-231666afa94b"
}
Changelog
Version history for Nexus API.
v0.1.0 March 2026 · Initial release
- Multi-channel notifications: push, SMS, email via OneSignal
- Async delivery via GCP Cloud Tasks with automatic retry
- Full audit log —
message_logwithqueued → sent | failedlifecycle - API key auth with SHA-256 hashing and per-key rate limiting
- Token-bucket rate limiter with
Retry-Afterheader - Structured JSON logging with request ID correlation
- Docker Compose local dev stack with Cloud Tasks bypass
- Terraform IaC for Cloud Run, Cloud SQL, Cloud Tasks, Secret Manager
- Alembic async migrations with asyncpg-safe patterns
- OpenAPI 3.1 spec with Swagger UI and ReDoc
Send push notification
Queue a push notification to one or more recipients via OneSignal.
Request headers
| Header | Value | Required |
|---|---|---|
X-API-Key | string | Required |
Content-Type | application/json | Required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
recipients | array[string] | Required | OneSignal External IDs (= Firebase UIDs) |
title | string | Required | Notification title |
body | string | Required | Notification body text |
subtitle | string | Optional | Subtitle (iOS only) |
data | object | Optional | Custom key-value payload for deep linking |
image_url | string | Optional | Image URL to display in the notification |
Response body
| Field | Type | Description |
|---|---|---|
message_log_id | integer | Database ID for this notification |
provider | string | Provider used, e.g. onesignal |
status | string | Always queued on 202 response |
Example request
curl -X POST https://dev-api.myratecards.com/push/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": ["JEMOPGeBCFSIi4LmS6W7xBV7bav2"],
"title": "New offer received",
"body": "John Smith has sent you a rate card offer.",
"data": { "screen": "offers", "offer_id": "abc123" }
}'
Example response
{
"success": true,
"data": { "message_log_id": 42, "provider": "onesignal", "status": "queued" },
"request_id": "13ab78ee-d790-4bf7-9237-39d2278d8fce"
}
Batch push notifications
Send multiple push notifications in a single request.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
notifications | array[object] | Required | Array of push objects — each uses the same schema as /push/send |
Example request
curl -X POST https://dev-api.myratecards.com/push/batch \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"notifications": [
{ "recipients": ["user-1"], "title": "Offer accepted", "body": "Your offer was accepted!" },
{ "recipients": ["user-2","user-3"], "title": "New message", "body": "You have a new message." }
]
}'
Send SMS
Queue an SMS message to one or more recipients.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
recipients | array[string] | Required | OneSignal External IDs |
message | string | Required | SMS message body text |
Example request
curl -X POST https://dev-api.myratecards.com/sms/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "recipients": ["JEMOPGeBCFSIi4LmS6W7xBV7bav2"], "message": "Your verification code is 482910" }'
{ "success": true, "data": { "message_log_id": 7, "provider": "onesignal", "status": "queued" }, "request_id": "b5c812f1-..." }
Batch SMS
Send multiple SMS messages in a single request.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
messages | array[object] | Required | Array of SMS objects — each uses the same schema as /sms/send |
Send email
Queue an email to one or more recipients. Two modes: direct (supply subject + body) or template (supply template_id with optional template_data for variable substitution).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
recipients | array[string] | Required | OneSignal External IDs |
subject | string | Optional* | Email subject line — required in direct mode |
body | string | Optional* | Email body (HTML supported) — required in direct mode |
template_id | string | Optional* | OneSignal template UUID — required in template mode |
template_data | object | Optional | Key-value pairs substituted into the template as {{message.custom_data.key}} |
* Either template_id or both subject + body must be provided.
Example — direct mode
curl -X POST https://dev-api.myratecards.com/email/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": ["JEMOPGeBCFSIi4LmS6W7xBV7bav2"],
"subject": "Your invoice is ready",
"body": "<h1>Invoice #1042</h1><p>Your invoice is ready to view.</p>"
}'
Example — template mode
curl -X POST https://dev-api.myratecards.com/email/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": ["JEMOPGeBCFSIi4LmS6W7xBV7bav2"],
"template_id": "your-onesignal-template-uuid",
"template_data": { "first_name": "Aju", "offer_amount": "$2,500" }
}'
{ "success": true, "data": { "message_log_id": 11, "provider": "onesignal", "status": "queued" }, "request_id": "96eef609-..." }
Health check
Check if the API is live. No authentication required.
Response body
| Field | Type | Description |
|---|---|---|
status | string | Always ok |
version | string | Current semver version |
environment | string | dev | staging | prod |
Example
curl https://dev-api.myratecards.com/health
{"status":"ok","version":"0.1.0","environment":"dev"}
Task worker endpoints
Internal endpoints called exclusively by GCP Cloud Tasks. Never call these from client applications.
Internal only. These are protected by a shared secret and called automatically by GCP after a message is enqueued.
Authentication
| Header | Value | Required |
|---|---|---|
X-CloudTasks-Token | Shared secret string | Required |
Request body
| Field | Type | Description |
|---|---|---|
message_log_id | integer | MessageLog ID to update after delivery |
provider | string | Provider to call, e.g. onesignal |
request | object | Original send request payload |
request_id | string | Original UUID for log correlation |
Docker Compose setup
Run the full Nexus stack locally using Docker Compose.
Prerequisites
| Tool | Version | Install |
|---|---|---|
| Docker Desktop | 27.x+ | brew install --cask docker |
| Python | 3.11+ | brew install python@3.11 |
Stack containers
| Container | Port | Description |
|---|---|---|
nexus-api | 8000 | FastAPI app with hot reload |
nexus-postgres | 5432 | PostgreSQL 15-alpine |
nexus-cloudtasks | 8123 | Cloud Tasks emulator (bypassed in dev via httpx) |
Quick start
git clone https://github.com/your-org/nexus-api.git
cd nexus-api
cp .env.example .env.dev
make docker-up
make docker-migrate
curl http://localhost:8000/health
Environment variables
All configuration is injected via environment variables. Copy .env.example to .env.dev and fill in your values.
| Variable | Required | Description |
|---|---|---|
ENVIRONMENT | Required | dev | staging | prod |
DATABASE_URL | Required | AsyncPG connection string |
ONESIGNAL_APP_ID | Required | OneSignal application UUID |
ONESIGNAL_API_KEY | Required | OneSignal REST API key |
CLOUD_TASKS_TOKEN | Required | Shared secret for X-CloudTasks-Token validation |
CLOUD_TASKS_BASE_URL | Required | Base URL Cloud Tasks uses to call back the API |
CLOUD_TASKS_QUEUE | Required | Full GCP queue resource name |
CLOUD_TASKS_EMULATOR | Optional | Set true locally to bypass Cloud Tasks |
LOG_LEVEL | Optional | Default INFO |
Local .env.dev example
ENVIRONMENT=dev
DATABASE_URL=postgresql+asyncpg://nexus:nexus@localhost:5432/nexus_dev
ONESIGNAL_APP_ID=14ccf8bd-e39b-4fa6-bf92-5f7462060cb5
ONESIGNAL_API_KEY=os_v2_app_...
CLOUD_TASKS_TOKEN=your-random-secret
CLOUD_TASKS_BASE_URL=http://localhost:8000
CLOUD_TASKS_QUEUE=projects/rate-cards-app-6buq10/locations/us-central1/queues/nexus-dev
CLOUD_TASKS_EMULATOR=true
Makefile commands
All common development tasks are wrapped in Makefile targets.
| Command | Description |
|---|---|
make docker-up | Start all containers in background |
make docker-down | Stop and remove all containers |
make docker-migrate | Run alembic upgrade head inside the api container |
make docker-shell | Open bash shell inside nexus-api container |
make docker-logs | Follow nexus-api logs |
make docker-test | Run pytest inside the container |
make docker-docs | Generate OpenAPI JSON, YAML, and ReDoc HTML |
GCP prerequisites
Install required tools and authenticate before running any GCP commands.
Install gcloud CLI and Terraform
export CLOUDSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13
brew reinstall --cask google-cloud-sdk
brew tap hashicorp/tap && brew install hashicorp/tap/terraform
gcloud version && terraform version
Authenticate and set project
gcloud auth login
gcloud config set project rate-cards-app-6buq10
gcloud auth application-default login
Enable required GCP APIs
gcloud services enable \
run.googleapis.com sqladmin.googleapis.com cloudtasks.googleapis.com \
secretmanager.googleapis.com cloudbuild.googleapis.com \
artifactregistry.googleapis.com vpcaccess.googleapis.com \
servicenetworking.googleapis.com \
--project=rate-cards-app-6buq10
Infrastructure setup
One-time setup of supporting infrastructure before Terraform.
Create Artifact Registry repository
gcloud artifacts repositories create nexus \
--repository-format=docker --location=us-central1 \
--project=rate-cards-app-6buq10
Set up VPC peering for Cloud SQL private IP
gcloud compute addresses create google-managed-services-default \
--global --purpose=VPC_PEERING --prefix-length=16 \
--network=default --project=rate-cards-app-6buq10
gcloud services vpc-peerings connect \
--service=servicenetworking.googleapis.com \
--ranges=google-managed-services-default \
--network=default --project=rate-cards-app-6buq10
Create Terraform state bucket
gcloud storage buckets create gs://nexus-api-tfstate-rate-cards \
--location=us-central1 --project=rate-cards-app-6buq10
Create VPC connector (Cloud Run → Cloud SQL)
gcloud compute networks vpc-access connectors create nexus-connector \
--region=us-central1 --network=default \
--range=10.8.0.0/28 --project=rate-cards-app-6buq10
Build & push image
Build a linux/amd64 image and push to Artifact Registry. Required even on Apple Silicon.
gcloud auth configure-docker us-central1-docker.pkg.dev
docker buildx build \
--platform linux/amd64 \
-t us-central1-docker.pkg.dev/rate-cards-app-6buq10/nexus/nexus-api:dev-latest \
--push .
Apple Silicon: Always use --platform linux/amd64. Cloud Run only runs AMD64 containers. Without this flag your ARM build will fail on Cloud Run.
Redeploy after code changes
All infrastructure changes must go through Terraform. Never use gcloud run services update directly.
docker buildx build --platform linux/amd64 \
-t us-central1-docker.pkg.dev/rate-cards-app-6buq10/nexus/nexus-api:dev-latest \
--push .
cd terraform
terraform apply -var-file="environments/dev.tfvars"
Never use gcloud directly: Running gcloud run services update bypasses Terraform state and will cause drift. Always deploy via terraform apply.
Terraform apply
Provision Cloud Run, Cloud SQL, Cloud Tasks queue, Secret Manager secrets, and IAM bindings.
Configure dev.tfvars
project_id = "rate-cards-app-6buq10"
region = "us-central1"
env = "dev"
container_image = "us-central1-docker.pkg.dev/rate-cards-app-6buq10/nexus/nexus-api:dev-latest"
vpc_network = "projects/rate-cards-app-6buq10/global/networks/default"
cloudsql_tier = "db-f1-micro"
db_password = "YOUR_DB_PASSWORD"
database_url = "postgresql+asyncpg://nexus:YOUR_DB_PASSWORD@/nexus_dev?host=/cloudsql/rate-cards-app-6buq10:us-central1:nexus-dev"
onesignal_app_id = "YOUR_ONESIGNAL_APP_ID"
onesignal_api_key = "YOUR_ONESIGNAL_API_KEY"
cloud_tasks_token = "YOUR_RANDOM_SECRET"
cloud_tasks_base_url = "https://dev-api.myratecards.com"
custom_domain = "dev-api.myratecards.com"
max_instances = 3
cpu = "1"
memory = "512Mi"
Run Terraform
cd terraform
terraform init \
-backend-config="bucket=nexus-api-tfstate-rate-cards" \
-backend-config="prefix=terraform/state/dev"
terraform plan -var-file="environments/dev.tfvars"
terraform apply -var-file="environments/dev.tfvars"
Custom domain: Set cloud_tasks_base_url in dev.tfvars to your custom domain (e.g. https://dev-api.myratecards.com) before applying. Verify ownership of the root domain in Google Search Console first, then add the CNAME record in Route 53 after apply.
Grant Cloud SQL access
gcloud projects add-iam-policy-binding rate-cards-app-6buq10 \
--member="serviceAccount:nexus-api-dev@rate-cards-app-6buq10.iam.gserviceaccount.com" \
--role="roles/cloudsql.client"
Run migrations
Run Alembic migrations against Cloud SQL using a one-off Cloud Run job.
gcloud run jobs create nexus-migrate \
--image=us-central1-docker.pkg.dev/rate-cards-app-6buq10/nexus/nexus-api:dev-latest \
--region=us-central1 \
--set-cloudsql-instances=rate-cards-app-6buq10:us-central1:nexus-dev \
--set-secrets="DATABASE_URL=DATABASE_URL:latest" \
--service-account=nexus-api-dev@rate-cards-app-6buq10.iam.gserviceaccount.com \
--vpc-connector=nexus-connector \
--command="alembic" --args="upgrade,head" \
--project=rate-cards-app-6buq10
gcloud run jobs execute nexus-migrate \
--region=us-central1 --project=rate-cards-app-6buq10 --wait
Re-running: For future schema changes, rebuild the image with new migration files, push it, then run gcloud run jobs execute nexus-migrate --wait again.
View migration logs
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="nexus-migrate"' \
--project=rate-cards-app-6buq10 --limit=20 \
--format="value(textPayload)" --freshness=5m
Seed database
Insert the initial API key into Cloud SQL using the Cloud SQL Auth Proxy.
Enable public IP on the Cloud SQL instance
gcloud sql instances patch nexus-dev --assign-ip --project=rate-cards-app-6buq10
Download Cloud SQL Auth Proxy (Apple Silicon)
curl -o cloud-sql-proxy \
https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.8.1/cloud-sql-proxy.darwin.arm64
chmod +x cloud-sql-proxy
Start proxy on port 5434
Port 5432 is already used by local Docker Postgres.
./cloud-sql-proxy rate-cards-app-6buq10:us-central1:nexus-dev --port=5434 &
sleep 5
Seed via psql env vars
Using env vars avoids zsh ! history expansion issues with passwords.
export PGHOST=localhost PGPORT=5434 PGDATABASE=nexus_dev
export PGUSER=nexus PGPASSWORD='YOUR_DB_PASSWORD'
cat > /tmp/seed.sql << 'EOF'
INSERT INTO api_key (name, key_hash, is_active, rate_limit_per_minute)
VALUES (
'default',
encode(sha256('YOUR_RAW_API_KEY'::bytea), 'hex'),
true, 60
) ON CONFLICT DO NOTHING;
EOF
psql -f /tmp/seed.sql
Verify deployment
Confirm the full end-to-end stack is working on GCP.
Health check
curl https://dev-api.myratecards.com/health
Live push notification test
curl -X POST https://dev-api.myratecards.com/push/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "recipients":["YOUR_ONESIGNAL_EXTERNAL_ID"], "title":"Hello from GCP", "body":"Nexus is live!" }'
View live logs
gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="nexus-api"' \
--project=rate-cards-app-6buq10 --limit=30 \
--format="value(textPayload)" --freshness=10m
gcloud emergency commands
These commands bypass Terraform and should only be used in emergencies — e.g. a broken deploy that needs an immediate rollback, or a one-off diagnostic. Any changes made here will cause Terraform state drift and must be reconciled with terraform apply afterward.
Do not use these commands for routine deployments. All infrastructure changes must go through terraform apply -var-file="environments/dev.tfvars". Using gcloud directly will cause state drift and may break future Terraform runs.
Force redeploy image (emergency rollback)
gcloud run services update nexus-api \
--image=us-central1-docker.pkg.dev/rate-cards-app-6buq10/nexus/nexus-api:dev-latest \
--region=us-central1 \
--project=rate-cards-app-6buq10
View live logs
gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="nexus-api"' \
--project=rate-cards-app-6buq10 --limit=50 \
--format="value(textPayload)" --freshness=10m
Update a secret value
echo -n "new-secret-value" | gcloud secrets versions add SECRET_NAME \
--data-file=- --project=rate-cards-app-6buq10
# Cloud Run picks up "latest" version automatically on next cold start
Describe current Cloud Run revision
gcloud run services describe nexus-api \
--region=us-central1 \
--project=rate-cards-app-6buq10
Patch Cloud SQL (break-glass only)
gcloud sql instances patch nexus-dev \
--project=rate-cards-app-6buq10 \
[--assign-ip | --no-assign-ip | other flags]
After any emergency gcloud operation, run terraform plan -var-file="environments/dev.tfvars" to check for drift and terraform apply to reconcile state.
Dev / Staging / Production
Infrastructure configuration differences across environments.
| Setting | Dev | Staging | Prod |
|---|---|---|---|
| Cloud SQL tier | db-f1-micro | db-g1-small | db-custom-2-7680 |
| SQL availability | ZONAL | ZONAL | REGIONAL |
| DB backups | ❌ | ❌ | ✅ |
| Deletion protection | ❌ | ❌ | ✅ |
| Min instances | 0 | 0 | 1 (always warm) |
| Max instances | 3 | 5 | 20 |
| Memory | 512Mi | 512Mi | 1Gi |
| CPU | 1 | 1 | 2 |
| Custom domain | — | — | api.ratecards.app |
GCP resources per environment
| Resource | Dev | Staging | Prod |
|---|---|---|---|
| Cloud Run service | nexus-api | nexus-api-staging | nexus-api-prod |
| Cloud SQL instance | nexus-dev | nexus-staging | nexus-prod |
| Cloud Tasks queue | nexus-dev | nexus-staging | nexus-prod |
| Service account | nexus-api-dev@... | nexus-api-staging@... | nexus-api-prod@... |
| Terraform state | terraform/state/dev | terraform/state/staging | terraform/state/prod |
Secrets management
All credentials are stored in GCP Secret Manager and injected into Cloud Run at runtime via --set-secrets.
| Secret name | Description |
|---|---|
DATABASE_URL | Full asyncpg connection string including Cloud SQL Unix socket path |
ONESIGNAL_APP_ID | OneSignal application UUID |
ONESIGNAL_API_KEY | OneSignal REST API key (os_v2_app_...) |
CLOUD_TASKS_TOKEN | Shared secret for X-CloudTasks-Token header validation |
Update a secret
echo -n "new-value" | gcloud secrets versions add SECRET_NAME \
--data-file=- --project=rate-cards-app-6buq10
# Cloud Run automatically uses the "latest" version
IAM roles required
| Role | Purpose |
|---|---|
roles/secretmanager.secretAccessor | Cloud Run reads secrets at startup |
roles/cloudsql.client | Cloud Run and migration job connect to Cloud SQL |
roles/cloudtasks.enqueuer | Cloud Run enqueues tasks to the queue |
roles/logging.logWriter | Cloud Run writes structured logs to Cloud Logging |
roles/run.invoker (allUsers) | Public access to the Cloud Run service |
March 2026
Release notes for Nexus API — March 2026.
First production deployment of Nexus API on GCP Cloud Run. Full multi-channel notification support via OneSignal across push, SMS, and email.
- Push notifications —
POST /push/sendandPOST /push/batchwith OneSignal External ID targeting. Supports title, body, subtitle, data payload, and image URL. - SMS notifications —
POST /sms/sendandPOST /sms/batchvia OneSignal SMS channel. - Email notifications —
POST /email/sendsupports both direct (HTML body) and template modes. Passtemplate_id+ optionaltemplate_datafor OneSignal template-based emails with variable substitution via{{message.custom_data.key}}. - Async delivery via Cloud Tasks — All send calls return
202 Acceptedimmediately. GCP Cloud Tasks handles retry logic with exponential backoff. - Full audit log — Every notification creates a
message_logrecord trackingqueued → sent | failedlifecycle with timestamps and error details. - API key authentication — SHA-256 hashed keys stored in PostgreSQL. Keys are managed directly in the database with instant revocation via
is_activeflag. - Per-key rate limiting — Token-bucket algorithm enforces configurable
rate_limit_per_minuteper API key. Returns429withRetry-Afterheader. - Request ID correlation — Every request and response carries a UUID
request_idfor end-to-end tracing in Cloud Logging. - Health endpoint —
GET /healthreturns version and environment. No authentication required. Used by Cloud Run health checks.
- GCP Cloud Run — Containerized FastAPI service on
us-central1. Scales to zero ondev, minimum 1 instance onprod. - Cloud SQL PostgreSQL 15 — Private IP via VPC peering. Managed via Alembic async migrations using asyncpg-safe patterns.
- GCP Secret Manager — All credentials (
DATABASE_URL,ONESIGNAL_APP_ID,ONESIGNAL_API_KEY,CLOUD_TASKS_TOKEN) injected at runtime. - VPC connector —
nexus-connector(10.8.0.0/28) enables Cloud Run to Cloud SQL private IP connectivity. - Terraform IaC — Complete infrastructure-as-code in
terraform/with per-environment.tfvarsfiles. State stored in GCS bucket. - Docker Compose local stack — Full local development environment with PostgreSQL, Cloud Tasks emulator bypass via httpx, and hot reload.
- OpenAPI 3.1 spec with Swagger UI at
/docsand ReDoc at/redoc - Makefile with
docker-up,docker-migrate,docker-test,docker-shelltargets - Structured JSON logging with request ID, channel, provider, and status fields
- Consistent error envelope with machine-readable
error.codeacross all endpoints
- Cloud SQL public IP is enabled on
devinstance for local proxy access. Will be disabled after CI/CD pipeline is established. - Migration Cloud Run job (
nexus-migrate) must be re-executed manually after each schema change. CI/CD automation planned. - No test coverage yet — pytest scaffold exists but test suite is empty.
Roadmap
Planned features and improvements for upcoming Nexus API releases.
- Custom domain — Map
api.ratecards.appto the Cloud Run service via GCP Load Balancer and managed SSL. - FlutterFlow integration — Replace direct OneSignal calls in
lib/backend/api_requests/api_calls.dartwith Nexus endpoints. Custom actions for push, SMS, and email. - CI/CD pipeline — Cloud Build trigger on
mainbranch push: build AMD64 image → push to Artifact Registry → run migration job → deploy Cloud Run revision. - Staging environment — Replicate dev Terraform config for staging with separate Cloud SQL instance, queue, and secrets.
- Test suite — pytest coverage for service layer, provider mock, auth middleware, and rate limiter using
asyncpgtest fixtures. - Message log API —
GET /logs/{message_log_id}endpoint to query delivery status by ID. Useful for FlutterFlow delivery receipts.
- Additional providers — Twilio for SMS, SendGrid for email. Provider registry with per-channel fallback logic.
- Webhook delivery — Inbound webhook endpoint to receive OneSignal delivery confirmations and update
message_logstatus in real time. - Admin API — Endpoints for API key management (create, list, revoke) without direct database access.
- Multi-tenant support — Namespace API keys and message logs by tenant ID for white-label deployments.
- Scheduled notifications —
send_atfield on send requests. Cloud Tasks supports up to 30-day scheduled delivery.