The Rails backend API service for Telurify, a platform that exposes worldwide seismic activity data collected from the USGS Earthquake Hazards Program. Events are exposed through a JSON API and allow users to submit structured "Did You Feel It?" intensity reports.
Note: The frontend application lives in a separate repository (
telurify-web), built with Astro and React islands.
flowchart LR
subgraph Backend["Backend (Rails 7.2 API-only)"]
RC[SismosController<br/>GET /v1/sismos]
ReportsController[ReportsController<br/>POST /v1/sismos/:id/reports]
SM[Sismo Model]
RM[Report Model]
RA[Rack::Attack<br/>rate limiting]
end
subgraph Data["Data Layer"]
PG[(PostgreSQL 16)]
Redis[(Redis / Upstash)]
end
subgraph External["External Services"]
Ingestion[Telurify Ingestion]
USGS[USGS Earthquake Hazards Program]
end
RC --> SM
ReportsController --> RM
ReportsController --> RA
RA -->|counters| Redis
SM --> PG
RM --> PG
Ingestion -->|fetch & validate| USGS
Ingestion -->|persist| PG
Component responsibilities:
| Layer | Responsibility |
|---|---|
| Backend API | Serves paginated, filterable seismic events in a JSON:API-style format and accepts structured intensity reports for events. |
| Ingestion service | The Telurify Ingestion service collects, validates, and persists seismic events. |
| Rack::Attack | Rate-limits all requests by IP (60 req/min) and throttles the reports endpoint specifically (5 req/min) to prevent spam on a public, unauthenticated endpoint. |
| PostgreSQL | Stores sismos (events) and reports. |
| Redis (Upstash) | Backs rack-attack's distributed rate-limit counters in production. |
Backend
- Ruby 3.4.10 / Rails 7.2.3 (API-only mode)
- PostgreSQL 16
will_paginate,rack-corsrack-attack+redis(rate limiting, backed by Upstash in production)- Linting/security:
rubocop,brakeman,bundler-audit
Infrastructure
- Docker Compose (dev environment:
db,backend) - Makefile as the single entry point for all workflows
- GitHub Actions CI (tests + linting/security)
- Render (API hosting) + Neon (PostgreSQL) + Upstash (Redis) in production
Only Docker and Make are required — no local Ruby or PostgreSQL installation needed. All dependencies run inside containers, and the source code is bind-mounted so changes inside containers (e.g., Gemfile.lock) are reflected in your local directory.
make dev-setupThis single command will:
- Build the dev Docker images
- Start PostgreSQL and wait for it to be healthy
- Create the databases and run migrations
- Start all backend services (
db,backend)
Note on Redis: no local Redis is required for development.
rack-attackfalls back to an in-memory store automatically whenREDIS_URL/RACK_ATTACK_REDIS_URLare unset.
| App | URL |
|---|---|
| Backend API | http://localhost:3000/v1/sismos |
| Command | Description |
|---|---|
make dev-setup |
Full setup: build images, create DB, run migrations, start all services |
make dev-up |
Start the dev environment |
make dev-down |
Stop the dev environment (keeps DB volume) |
make dev-down-clean |
Stop containers and remove volumes (fresh start) |
make dev-build |
Rebuild Docker images (needed after changing the Gemfile) |
make dev-install |
Install/update dependencies inside the containers |
make dev-shell-backend |
Open a shell in the backend container |
Dependencies are updated inside the container; lockfiles are updated on your host via bind mounts:
make dev-shell-backend
bundle update # or: bundle update <gem>
# Gemfile.lock on your host is now updatedList seismic events
GET /v1/sismos
Query parameters:
| Param | Description |
|---|---|
page |
Page number (default: 1) |
per_page |
Items per page (max: 1000) |
filters[mag_type] |
Comma-separated magnitude types: md, ml, ms, mw, me, mi, mb, mlg |
curl 'http://localhost:3000/v1/sismos?page=1&per_page=10&filters[mag_type]=mw,ml'Response:
{
"data": [
{
"id": 1,
"type": "feature",
"attributes": {
"external_id": "ci40664762",
"magnitude": 0.93,
"place": "10 km N of Banning, CA",
"time": "2026-08-01 17:43:53 UTC",
"tsunami": false,
"mag_type": "ml",
"title": "M 0.9 - 10 km N of Banning, CA",
"coordinates": {
"longitude": -116.85,
"latitude": 34.01
}
},
"links": {
"external_url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40664762"
}
}
],
"pagination": {
"current_page": 1,
"total": 12847,
"per_page": 10
}
}Submit an intensity report for an event
POST /v1/sismos/:sismo_id/reports
curl -X POST 'http://localhost:3000/v1/sismos/1/reports' \
-H 'Content-Type: application/json' \
-d '{"felt": true, "intensity": "moderate"}'201 Created— report persisted (felt: boolean,intensity: one ofnot_felt,weak,light,moderate,strong,severe)422 Unprocessable Entity— validation failed404 Not Found— the referenced event does not exist429 Too Many Requests— rate limit exceeded (see Rate Limiting)
Register a web notification device
POST /v1/devicescurl -X POST 'http://localhost:3000/v1/devices' \
-H 'Content-Type: application/json' \
-d '{"fcm_token":"token-from-firebase-messaging"}'The token is unique and is stored with platform: "web".
List or remove notification devices
GET /v1/devices
DELETE /v1/devices/:idThese endpoints require the administrative header:
X-Admin-Token: <ADMIN_TOKEN>GET /v1/devices returns only the FCM token strings so the notification
service can send alerts without accessing the database directly.
Public write endpoints are protected against abuse via rack-attack, backed by Redis for distributed counters:
| Rule | Limit | Scope |
|---|---|---|
| Global | 60 requests/minute | Per IP, all endpoints except /assets |
| Reports | 5 requests/minute | Per IP, POST /v1/sismos/:id/reports only |
| Devices | 5 requests/minute | Per IP, POST /v1/devices only |
Exceeding a limit returns 429 Too Many Requests with a Retry-After header and a JSON error body:
{ "error": "Rate limit exceeded. Try again in 42 seconds." }Set one of these environment variables to a Redis connection string:
| Variable | Description |
|---|---|
REDIS_URL |
Standard Redis connection string |
RACK_ATTACK_REDIS_URL |
Takes precedence if set — use this to point rate limiting at a different Redis instance than other Redis usage |
In production, this points to an Upstash Redis instance (free tier). REDIS_URL (or RACK_ATTACK_REDIS_URL) is required in production — the app raises on boot if neither is set and RAILS_ENV=production.
In development, if neither variable is set, rate limiting falls back to an in-memory store (no Redis needed locally).
# Run the test suite
docker compose exec backend bin/rails test
# Lint & security checks (same as CI)
docker compose exec backend bin/rubocop --parallel
docker compose exec backend bin/brakeman -q -w2
docker compose exec backend bin/bundler-auditThe GitHub Actions workflow (.github/workflows/ci.yml) runs the test suite and all three linting/security checks on every push and pull request to main.
This project is released under the MIT License.