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.

1
Client sends request — Your app calls POST /push/send (or /sms/send, /email/send) with an X-API-Key header.
2
API key validated — The SHA-256 hash is looked up in api_key table. Returns 401 if missing, 403 if invalid or inactive.
3
Rate limit checked — Token-bucket limiter enforces per-key requests-per-minute. Returns 429 with Retry-After if exceeded.
4
MessageLog created — A record is written to PostgreSQL with status = queued and a unique id.
5
Cloud Task enqueued — GCP Cloud Task is enqueued pointing to POST /tasks/{channel} with message_log_id and payload.
6
202 Accepted returned — API responds immediately with message_log_id, provider, and status: "queued".
7
Cloud Tasks fires the worker — GCP delivers POST /tasks/{channel}. The X-CloudTasks-Token shared secret is verified.
8
Provider called & log updated — Worker calls OneSignal and updates MessageLog to 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.

Project Structure
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

ColumnTypeDescription
idintegerPrimary key
namevarcharHuman-readable label
key_hashvarcharSHA-256 hash of the raw API key
is_activebooleanSet false to revoke instantly
rate_limit_per_minuteintegerMax requests/min for this key
created_attimestamptzCreation timestamp

message_log

ColumnTypeDescription
idintegerPrimary key, returned as message_log_id
channelenumpush | sms | email
providervarchare.g. onesignal
statusenumqueuedsent | failed
recipientsjsonbArray of recipient IDs
payloadjsonbFull request payload
error_messagetextProvider error detail, if any
created_attimestamptzRequest received timestamp
updated_attimestamptzLast status update

Authentication

All endpoints except GET /health require an API key in the X-API-Key request header.

HeaderValueRequired
X-API-KeystringRequired

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

StatusCodeCause
401UNAUTHORIZEDX-API-Key header missing entirely
403FORBIDDENKey found but invalid, revoked, or is_active = false
429RATE_LIMIT_EXCEEDEDKey exceeded per-minute quota — check Retry-After

Example

Bash
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.

EnvironmentBase URLNotes
Localhttp://localhost:8000Docker Compose · Mac
Devhttps://dev-api.myratecards.comGCP Cloud Run · us-central1  legacy: nexus-api-ttrqossa7a-uc.a.run.app
Staginghttps://stg-api.myratecards.comComing soon
Productionhttps://api.myratecards.comComing 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

Bash
curl https://dev-api.myratecards.com/health
Response · 200 OK
{"status":"ok","version":"0.1.0","environment":"dev"}

2. Send a push notification

Bash
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!"
  }'
Response · 202 Accepted
{
  "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

JSON
{ "success": true, "data": { ... }, "request_id": "uuid-v4" }
FieldTypeDescription
successbooleanAlways true
dataobjectEndpoint-specific payload
request_idstringUUID for tracing in Cloud Logging

Error envelope

JSON
{ "success": false, "error": { "code": "ERROR_CODE", "message": "..." }, "request_id": "uuid-v4" }
FieldTypeDescription
successbooleanAlways false
error.codestringMachine-readable error code
error.messagestringHuman-readable description
request_idstringUse 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.

HTTPCodeDescription
401UNAUTHORIZEDX-API-Key header missing entirely.
403FORBIDDENKey found but invalid, revoked, or is_active = false.
422VALIDATION_ERRORRequest body failed Pydantic validation. Check error.message for field details.
429RATE_LIMIT_EXCEEDEDExceeded per-minute quota. Check the Retry-After response header.
401TASK_AUTH_FAILEDX-CloudTasks-Token missing or wrong on a /tasks/* endpoint.
502PROVIDER_ERRORUpstream provider (OneSignal etc.) returned an error. Check error.message.
500INTERNAL_ERRORUnexpected server error. Use request_id to search Cloud Logging.

Example

JSON · 403 Forbidden
{
  "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_log with queued → sent | failed lifecycle
  • API key auth with SHA-256 hashing and per-key rate limiting
  • Token-bucket rate limiter with Retry-After header
  • 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.

POST/push/sendReturns 202 Accepted

Request headers

HeaderValueRequired
X-API-KeystringRequired
Content-Typeapplication/jsonRequired

Request body

FieldTypeRequiredDescription
recipientsarray[string]RequiredOneSignal External IDs (= Firebase UIDs)
titlestringRequiredNotification title
bodystringRequiredNotification body text
subtitlestringOptionalSubtitle (iOS only)
dataobjectOptionalCustom key-value payload for deep linking
image_urlstringOptionalImage URL to display in the notification

Response body

FieldTypeDescription
message_log_idintegerDatabase ID for this notification
providerstringProvider used, e.g. onesignal
statusstringAlways queued on 202 response

Example request

Bash
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

JSON · 202 Accepted
{
  "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.

POST/push/batchReturns 202 Accepted

Request body

FieldTypeRequiredDescription
notificationsarray[object]RequiredArray of push objects — each uses the same schema as /push/send

Example request

Bash
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.

POST/sms/sendReturns 202 Accepted

Request body

FieldTypeRequiredDescription
recipientsarray[string]RequiredOneSignal External IDs
messagestringRequiredSMS message body text

Example request

Bash
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" }'
JSON · 202 Accepted
{ "success": true, "data": { "message_log_id": 7, "provider": "onesignal", "status": "queued" }, "request_id": "b5c812f1-..." }

Batch SMS

Send multiple SMS messages in a single request.

POST/sms/batchReturns 202 Accepted

Request body

FieldTypeRequiredDescription
messagesarray[object]RequiredArray 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).

POST/email/sendReturns 202 Accepted

Request body

FieldTypeRequiredDescription
recipientsarray[string]RequiredOneSignal External IDs
subjectstringOptional*Email subject line — required in direct mode
bodystringOptional*Email body (HTML supported) — required in direct mode
template_idstringOptional*OneSignal template UUID — required in template mode
template_dataobjectOptionalKey-value pairs substituted into the template as {{message.custom_data.key}}

* Either template_id or both subject + body must be provided.

Example — direct mode

Bash
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

Bash
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" }
  }'
JSON · 202 Accepted
{ "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.

GET/health200 OK · No auth required

Response body

FieldTypeDescription
statusstringAlways ok
versionstringCurrent semver version
environmentstringdev | staging | prod

Example

Bash
curl https://dev-api.myratecards.com/health
JSON · 200 OK
{"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.

POST/tasks/push
POST/tasks/sms
POST/tasks/email

Authentication

HeaderValueRequired
X-CloudTasks-TokenShared secret stringRequired

Request body

FieldTypeDescription
message_log_idintegerMessageLog ID to update after delivery
providerstringProvider to call, e.g. onesignal
requestobjectOriginal send request payload
request_idstringOriginal UUID for log correlation

Docker Compose setup

Run the full Nexus stack locally using Docker Compose.

Prerequisites

ToolVersionInstall
Docker Desktop27.x+brew install --cask docker
Python3.11+brew install python@3.11

Stack containers

ContainerPortDescription
nexus-api8000FastAPI app with hot reload
nexus-postgres5432PostgreSQL 15-alpine
nexus-cloudtasks8123Cloud Tasks emulator (bypassed in dev via httpx)

Quick start

Bash
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.

VariableRequiredDescription
ENVIRONMENTRequireddev | staging | prod
DATABASE_URLRequiredAsyncPG connection string
ONESIGNAL_APP_IDRequiredOneSignal application UUID
ONESIGNAL_API_KEYRequiredOneSignal REST API key
CLOUD_TASKS_TOKENRequiredShared secret for X-CloudTasks-Token validation
CLOUD_TASKS_BASE_URLRequiredBase URL Cloud Tasks uses to call back the API
CLOUD_TASKS_QUEUERequiredFull GCP queue resource name
CLOUD_TASKS_EMULATOROptionalSet true locally to bypass Cloud Tasks
LOG_LEVELOptionalDefault INFO

Local .env.dev example

Dotenv
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.

CommandDescription
make docker-upStart all containers in background
make docker-downStop and remove all containers
make docker-migrateRun alembic upgrade head inside the api container
make docker-shellOpen bash shell inside nexus-api container
make docker-logsFollow nexus-api logs
make docker-testRun pytest inside the container
make docker-docsGenerate OpenAPI JSON, YAML, and ReDoc HTML

GCP prerequisites

Install required tools and authenticate before running any GCP commands.

1

Install gcloud CLI and Terraform

Bash
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
2

Authenticate and set project

Bash
gcloud auth login
gcloud config set project rate-cards-app-6buq10
gcloud auth application-default login
3

Enable required GCP APIs

Bash
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.

1

Create Artifact Registry repository

Bash
gcloud artifacts repositories create nexus \
  --repository-format=docker --location=us-central1 \
  --project=rate-cards-app-6buq10
2

Set up VPC peering for Cloud SQL private IP

Bash
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
3

Create Terraform state bucket

Bash
gcloud storage buckets create gs://nexus-api-tfstate-rate-cards \
  --location=us-central1 --project=rate-cards-app-6buq10
4

Create VPC connector (Cloud Run → Cloud SQL)

Bash
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.

Bash
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.

Bash — Step 1: Build & push image
docker buildx build --platform linux/amd64 \
  -t us-central1-docker.pkg.dev/rate-cards-app-6buq10/nexus/nexus-api:dev-latest \
  --push .
Bash — Step 2: Apply via Terraform
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

HCL · terraform/environments/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

Bash
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

Bash
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.

Bash
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

Bash
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.

1

Enable public IP on the Cloud SQL instance

Bash
gcloud sql instances patch nexus-dev --assign-ip --project=rate-cards-app-6buq10
2

Download Cloud SQL Auth Proxy (Apple Silicon)

Bash
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
3

Start proxy on port 5434

Port 5432 is already used by local Docker Postgres.

Bash
./cloud-sql-proxy rate-cards-app-6buq10:us-central1:nexus-dev --port=5434 &
sleep 5
4

Seed via psql env vars

Using env vars avoids zsh ! history expansion issues with passwords.

Bash
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

Bash
curl https://dev-api.myratecards.com/health

Live push notification test

Bash
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

Bash
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)

Bash
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

Bash
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

Bash
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

Bash
gcloud run services describe nexus-api \
  --region=us-central1 \
  --project=rate-cards-app-6buq10

Patch Cloud SQL (break-glass only)

Bash
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.

SettingDevStagingProd
Cloud SQL tierdb-f1-microdb-g1-smalldb-custom-2-7680
SQL availabilityZONALZONALREGIONAL
DB backups
Deletion protection
Min instances001 (always warm)
Max instances3520
Memory512Mi512Mi1Gi
CPU112
Custom domainapi.ratecards.app

GCP resources per environment

ResourceDevStagingProd
Cloud Run servicenexus-apinexus-api-stagingnexus-api-prod
Cloud SQL instancenexus-devnexus-stagingnexus-prod
Cloud Tasks queuenexus-devnexus-stagingnexus-prod
Service accountnexus-api-dev@...nexus-api-staging@...nexus-api-prod@...
Terraform stateterraform/state/devterraform/state/stagingterraform/state/prod

Secrets management

All credentials are stored in GCP Secret Manager and injected into Cloud Run at runtime via --set-secrets.

Secret nameDescription
DATABASE_URLFull asyncpg connection string including Cloud SQL Unix socket path
ONESIGNAL_APP_IDOneSignal application UUID
ONESIGNAL_API_KEYOneSignal REST API key (os_v2_app_...)
CLOUD_TASKS_TOKENShared secret for X-CloudTasks-Token header validation

Update a secret

Bash
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

RolePurpose
roles/secretmanager.secretAccessorCloud Run reads secrets at startup
roles/cloudsql.clientCloud Run and migration job connect to Cloud SQL
roles/cloudtasks.enqueuerCloud Run enqueues tasks to the queue
roles/logging.logWriterCloud 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.

v0.1.0 March 9, 2026 Initial Release

First production deployment of Nexus API on GCP Cloud Run. Full multi-channel notification support via OneSignal across push, SMS, and email.

✨ New features
  • Push notificationsPOST /push/send and POST /push/batch with OneSignal External ID targeting. Supports title, body, subtitle, data payload, and image URL.
  • SMS notificationsPOST /sms/send and POST /sms/batch via OneSignal SMS channel.
  • Email notificationsPOST /email/send supports both direct (HTML body) and template modes. Pass template_id + optional template_data for OneSignal template-based emails with variable substitution via {{message.custom_data.key}}.
  • Async delivery via Cloud Tasks — All send calls return 202 Accepted immediately. GCP Cloud Tasks handles retry logic with exponential backoff.
  • Full audit log — Every notification creates a message_log record tracking queued → sent | failed lifecycle 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_active flag.
  • Per-key rate limiting — Token-bucket algorithm enforces configurable rate_limit_per_minute per API key. Returns 429 with Retry-After header.
  • Request ID correlation — Every request and response carries a UUID request_id for end-to-end tracing in Cloud Logging.
  • Health endpointGET /health returns version and environment. No authentication required. Used by Cloud Run health checks.
🏗 Infrastructure
  • GCP Cloud Run — Containerized FastAPI service on us-central1. Scales to zero on dev, minimum 1 instance on prod.
  • 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 connectornexus-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 .tfvars files. State stored in GCS bucket.
  • Docker Compose local stack — Full local development environment with PostgreSQL, Cloud Tasks emulator bypass via httpx, and hot reload.
🔧 Developer experience
  • OpenAPI 3.1 spec with Swagger UI at /docs and ReDoc at /redoc
  • Makefile with docker-up, docker-migrate, docker-test, docker-shell targets
  • Structured JSON logging with request ID, channel, provider, and status fields
  • Consistent error envelope with machine-readable error.code across all endpoints
🐛 Known issues
  • Cloud SQL public IP is enabled on dev instance 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.

v0.2.0 Planned Q2 2026 Planned
🔜 Planned features
  • Custom domain — Map api.ratecards.app to the Cloud Run service via GCP Load Balancer and managed SSL.
  • FlutterFlow integration — Replace direct OneSignal calls in lib/backend/api_requests/api_calls.dart with Nexus endpoints. Custom actions for push, SMS, and email.
  • CI/CD pipeline — Cloud Build trigger on main branch 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 asyncpg test fixtures.
  • Message log APIGET /logs/{message_log_id} endpoint to query delivery status by ID. Useful for FlutterFlow delivery receipts.
🔮 Future considerations
  • 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_log status 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 notificationssend_at field on send requests. Cloud Tasks supports up to 30-day scheduled delivery.