Skip to content

DESIGN_API: API Contract & Security ​

This document defines the interface between the camply-backend (FastAPI) and camply-frontend (React). It also outlines the security flow for Auth0 and optional invite-only access.

Python package boundaries ​

The existing ASGI application is camply.server.app:app, enabled by the server extra. This installation supports API browsing and scan management with SQLite. Operations overview/task-list responses use existing unavailable/error fields when distributed is absent unless CAMPLY_RUNTIME_MODE=standalone is active.

Runtime and hosting contract ​

CAMPLY_RUNTIME_MODE=standalone serves built root-based frontend assets and the API from Python. Known API routes keep their normal errors, /api/* misses never become HTML, and /metrics is unavailable. HTML navigation falls back to the entry page for root/deep links; missing asset paths return 404. GET/HEAD are supported, other static methods return 405, hashed /assets/ files are immutable, and entry HTML is not cached. Auth mode, CSRF, invitation checks, and administrator authorization remain independent of runtime.

GET /api/health checks database readiness in both runtimes with a two-second query budget, additionally checks supervised local loops in standalone, and returns HTTP 503 when unhealthy. Its existing response shape is unchanged. /api/ping remains dependency-independent liveness. Admin operations adds runtime_mode, capabilities, and local scheduler_status. Standalone returns broker_connected: null, unsupported reserved/scheduled counts as null, measured queue/running counts, and bounded local attempt history (1,000 entries/24 hours, cleared on restart). Historical trends use the existing unavailable response shape and are intentionally hidden by capability in the frontend. Failed loops display a real unhealthy state rather than a broker outage.

Administrator provider sync dispatches to the selected runtime. Repeated local sync requests share the active job identity and return promptly. Existing distributed broker/inspection/Prometheus behavior remains the default. See admin response details.

🎯 API Philosophy ​

  1. OpenAPI-First: Every endpoint must be documented so that the frontend TypeScript client can be automatically generated.
  2. Standardized Responses: All successful responses use ORJSONResponse for performance.
  3. Strict Validation: Every request payload must be a Pydantic v2 model.
  4. Auth-Gated: All user-specific endpoints are gated by the CurrentUserDep dependency.

πŸ”’ Security & Middleware ​

1. Authentication and local development ​

The owner selects none, session, or auth0 with CAMPLY_AUTH_MODE, independently of the environment. HTTP Basic authentication is unsupported.

  • Automatic login (none, default): every request uses the shared admin identity; no login/signup/logout UI.
  • Password sessions (session): the app posts username/password JSON to POST /api/login. The response contains the profile and sets a signed HTTP-only session cookie plus a CSRF cookie. The frontend confirms the cookie with GET /api/me before opening the dashboard; rejected cookies keep the user on the login form with setup guidance. Scan queries do not automatically retry 401/403 responses. A scan query's 401 clears the cached profile and scans, cancels any profile refresh, and returns the user to sign-in. Mutations require X-CSRF-Token matching the signed session. POST /api/logout clears the cookies. The shared account has no signup. Cookies have an absolute lifetime, and credential/secret changes invalidate them.
  • Auth0 (auth0): requests require an RS256 bearer token validated against issuer and API audience. Users are provisioned by subject on first access. The application starts sign-in on /auth and uses the Auth0 SDK.
  • GET /api/auth-config: public configuration returns auth_mode, auth0_domain, auth0_client_id, auth0_audience, auto_login, signup_enabled, and invite_only. The React SDK requests the returned audience. Auth0 mode must supply domain, audience, and client ID.
  • GET /api/me: the profile exposes is_invited, is_admin, and scanning_enabled. The existing database column remains is_early_access_user for compatibility with stored data.

Session cookies include a credential version derived by HMAC from the current salted password hash. Every authenticated session request checks this version against the database, so a password reset or change invalidates earlier cookies without adding a database column or exposing the hash. POST /api/change-password refreshes the current browser's cookies and preserves its CSRF token. Legacy cookies without a credential version require sign-in again. Stored bootstrap passwords replace the configured password for authentication; the configured username remains an alias. Session mode honors the stored administrator role, including bootstrap account demotions. Automatic login restores administrator access and ignores password-change flags; Auth0 also ignores these session-only flags.

2. Optional invite-only guard ​

CAMPLY_INVITE_ONLY defaults to false. When enabled, every /api/scans operation requires the user's is_invited flag. Uninvited authenticated callers receive 403 with ERR_INVITE_REQUIRED. The frontend gates dashboard and scan detail routes with the invitation request screen. Profiles and public metadata remain accessible. Existing scan ownership checks apply in both configurations.

3. Invitation requests (collection only) ​

POST /api/request-access stores a pending email request and accepts duplicates idempotently. It does not grant access or deliver notifications.

TODO: Provide an approval/revocation workflow, verified identity matching, and invitation notification delivery.


πŸ—οΈ Endpoint Definitions ​

1. Search & Metadata (Public/Auth-Light) ​

  • GET /api/search?query=<term>: Full-text search for campgrounds and recreation areas (uses the Search FTS table).
  • GET /api/search/explore: Paginated search with totals and facets. Optional west, south, east, and north filter the full matching set before pagination and aggregation. Supply all four finite bounds or none; longitude is within [-180, 180], latitude within [-90, 90], south < north, and west β‰  east. West > east crosses the antimeridian. Viewport filtering excludes missing/invalid coordinates and lat/lon of 0/0; unbounded search retains unmappable places.
  • GET /api/providers: List supported providers and their scanning capabilities.
  • GET /api/provider/{id}: Get a single provider by ID.
  • GET /api/campground/{provider}/{id}: Get a single campground.
  • GET /api/rec-area/{provider}/{id}: Get a recreation area.
  • GET /api/rec-area/{provider}/{id}/campgrounds: List campgrounds within a recreation area.

2. User & Profile (Auth-Required) ​

  • GET /api/me: Get current user profile, invitation status, administrator role, scanning eligibility, and password change status.
  • PATCH /api/me: Update user-specific settings (e.g., pushover_token). Requires password update first if must_change_password is set.
  • POST /api/change-password: Update user password and clear must_change_password (session mode).

3. Scan Management (Auth-Required + Optional Invitation) ​

  • GET /api/scans: List all scans belonging to the current user (paginated).
  • POST /api/scans: Create a new scan.
    • Logic: Backend calculates the target hash, creates/links the UniqueTarget (de-duplication), and creates the UserScan.
    • Returns 201 Created on success, 409 Conflict if a duplicate scan exists.
  • GET /api/scans/{id}: Detailed view of a scan, including recent scan_results.
  • Result expiration: User-facing results and found_count omit nights before the current UTC date or outside the target interval, then apply the saved minimum stay. This also applies to paused scans. Successful provider checks replace availability, including clearing results for an empty response; failed checks preserve the latest successful snapshot and timestamp.
  • PATCH /api/scans/{id}: Edit the campground/provider, dates, filters (min_stay_length, preferred_types, require_electric), or toggle is_active. All fields are optional. Target changes validate the merged date range and campground, then relink only this subscription to a new or existing UniqueTarget; shared targets and other subscribers remain unchanged. Duplicate subscriptions return 409, invalid date ranges return 422, and unknown campgrounds return 404. Editing preserves the scan ID, creation date, and active preference unless explicitly changed. Suspended users can edit saved scans, but cannot resume them.
  • DELETE /api/scans/{id}: Unsubscribe from a scan (returns 204 No Content).

4. Administration & Operations (Admin-Only) ​

  • GET /api/admin/overview: Summary counts (users, active scans, targets, 24h results).
  • GET /api/admin/users: List users with scanning status, scan counts, pagination, and search.
  • POST /api/admin/users: Create a new database user with password, scanning status, admin status, and optional forced password change (session mode only).
  • GET /api/admin/users/{id}: User detail with scans and audit history.
  • PATCH /api/admin/users/{id}: Update scanning access or administrator role, rejecting self-demotion. Password resets and must_change_password updates require session mode; requests containing either field outside session mode return 400 before applying changes. Password resets revoke existing sessions and default to requiring password rotation.
  • GET /api/admin/scans: List all system scans with filters and pagination.
  • GET /api/admin/scans/{id}: Scan detail including target and subscriber info.
  • PATCH /api/admin/scans/{id}: Update scan status (e.g., toggle is_active).
  • GET /api/admin/targets/{id}: Shared target detail and subscribers.
  • GET /api/admin/audit: Immutable admin audit event log with pagination.
  • GET /api/admin/operations: Celery worker states, queue depth, discovery metadata, and recent tasks.
  • GET /api/admin/operations/tasks: Paginated Valkey telemetry stream for recent task executions.
  • GET /api/admin/operations/tasks/{task_id}: Detailed telemetry for a single task execution.
  • GET /api/admin/trends: Server-side Prometheus query proxy for usage, api, worker, and provider metrics across 24h, 7d, 30d.

πŸ“¦ Request / Response DTOs (Pydantic) ​

All schemas are defined in src/camply/server/schemas.py.

ScanCreateRequest ​

python
class ScanCreateRequest(BaseModel):
    provider_id: int = Field(..., description="Provider identifier")
    campground_id: str = Field(..., description="Provider-internal campground ID")
    start_date: date = Field(..., description="Check-in date")
    end_date: date = Field(..., description="Check-out date")
    min_stay_length: int = Field(default=1, ge=1)
    preferred_types: list[str] = Field(default_factory=list)
    require_electric: bool = Field(default=False)

ScanResponse ​

python
class ScanResponse(BaseModel):
    id: UUID
    provider_id: int
    campground_id: str
    campground_name: str
    recreation_area_name: str
    start_date: date
    end_date: date
    is_active: bool
    min_stay_length: int
    preferred_types: list[str]
    require_electric: bool
    last_checked_at: datetime | None
    found_count: int
    created_at: datetime

ScanDetailResponse extends ScanResponse ​

Adds:

python
    results: list[ScanResultItem]

ScanResultItem ​

python
class ScanResultItem(BaseModel):
    campsite_id: str
    campsite_name: str
    available_dates: list[str]
    campsite_type: str | None
    is_electric: bool | None
    is_accessible: bool | None
    booking_url: str

User scan results and found_count apply the saved site preferences and minimum consecutive stay. Dates represent available nights in qualifying blocks; check-out is the day after the last night. Booking links are derived from the provider's campground URL without making a provider request. Legacy snapshots have an empty name and unknown attributes until refreshed. Admin shared-target views remain unfiltered.


πŸ”„ Client Generation Workflow ​

  1. Backend developer updates the FastAPI router.
  2. Run task backend:check to ensure types are correct.
  3. Start the backend: task backend:dev.
  4. Run: task frontend:codegen from the repository root.
    • Fetches http://localhost:8000/api/openapi.json.
    • Generates TypeScript types in src/frontend/lib/api/generated/schema.ts.
  5. Register new endpoints in src/frontend/lib/api.ts and src/frontend/lib/structs.ts.

πŸ—ΊοΈ Endpoint Summary ​

MethodPathAuthInvitation (when enabled)Purpose
GET/api/search?query=β€”β€”Search campgrounds/rec areas
GET/api/providersβ€”β€”List providers
GET/api/provider/{id}β€”β€”Single provider
GET/api/campground/{provider}/{id}β€”β€”Single campground
GET/api/rec-area/{provider}/{id}β€”β€”Single rec area
GET/api/rec-area/{provider}/{id}/campgroundsβ€”β€”Campgrounds in rec area
GET/api/meβœ“β€”Current user profile
PATCH/api/meβœ“β€”Update profile
POST/api/change-passwordβœ“β€”Change password (session)
GET/api/scansβœ“βœ“List user scans
POST/api/scansβœ“βœ“Create scan
GET/api/scans/{id}βœ“βœ“Scan detail
PATCH/api/scans/{id}βœ“βœ“Update scan
DELETE/api/scans/{id}βœ“βœ“Delete scan
GET/api/healthβ€”β€”Health check
GET/api/admin/overviewβœ“ (Admin)β€”Admin KPI overview
GET/api/admin/usersβœ“ (Admin)β€”List users with status
POST/api/admin/usersβœ“ (Admin)β€”Create user with credentials
GET/api/admin/users/{id}βœ“ (Admin)β€”User details and audit
PATCH/api/admin/users/{id}βœ“ (Admin)β€”Update user (toggle scanning)
GET/api/admin/scansβœ“ (Admin)β€”List all system scans
GET/api/admin/scans/{id}βœ“ (Admin)β€”Scan detail and target info
PATCH/api/admin/scans/{id}βœ“ (Admin)β€”Update scan (toggle active)
GET/api/admin/targets/{id}βœ“ (Admin)β€”Shared target detail
GET/api/admin/auditβœ“ (Admin)β€”Immutable audit event log
GET/api/admin/operationsβœ“ (Admin)β€”Celery worker states & queue
GET/api/admin/operations/tasksβœ“ (Admin)β€”Valkey telemetry stream
GET/api/admin/operations/tasks/{id}βœ“ (Admin)β€”Task execution telemetry
POST/api/admin/operations/sync-providersβœ“ (Admin)β€”Dispatch provider catalog sync
GET/api/admin/trendsβœ“ (Admin)β€”Prometheus metrics proxy

Admin contract and monitoring availability ​

Admin frontend types reference the generated OpenAPI schema. Run task api:generate after changing response models; task api:check verifies drift without starting a server and runs in CI. Overview returns suspended/dormant counts and the actual overdue threshold (cooldown plus two discovery intervals). Scan lists support target_status=active|overdue|dormant; target detail uses the same status values.

Operations reports broker connectivity separately from worker inspection and marks incomplete snapshots as stale. Queue/discovery outages preserve prior successful values with explicit availability flags. Task-stream outages return available=false, not an empty success. Prometheus outages retain the last successful trend series and timestamp with available=false. Celery inspection remains read-only and browser requests only reach FastAPI.

Admin workload fields are nullable: an unanswered or malformed inspection reply is unknown, while a responding worker’s empty task list is a measured zero. Undefined Prometheus samples retain their timestamps with value: null, preserving chart gaps and table coverage. Recent task records include an optional event_id and one-based attempt; older producer records use their Valkey stream ID for row identity and leave the attempt unknown. History readers exclude records older than 24 hours.

Geographic discovery (public) ​

GET /api/search/map/summary returns global filtered total, mappable_total, unmappable_total, and nullable circular bounds. GET /api/search/map adds required west, south, east, north, and zoom (0–19), returning an echoed viewport and at most 512 location/group features. Longitude west > east means an antimeridian crossing; west=-180/east=180 is the full world. Invalid/nonfinite request bounds return 422. Only finite catalog coordinates within Β±90/Β±180 other than lat/lon 0/0 are mapped.

Both routes and GET /api/search/map/members reuse the existing Explore predicates, including PostgreSQL fuzzy matching. Map membership ignores list pagination/sort. Groups aggregate every matching visible row in a server-selected geographic grid; singleton features carry the existing search-result metadata. Global counts differ from visible represented counts. Member requests carry cell_id, the group's captured viewport, and the same filters, plus page_size (default 50, maximum 100) and optional after_id (the previous next_cursor). This keyset interface keeps co-located places reachable without fetching every list page or transferring the catalog.

These routes expose public catalog metadata and make no campsite-availability claim. Each request reflects the current catalog; no cross-request snapshot is guaranteed during metadata refresh. Existing protected scan endpoints still enforce monitoring access. See the mapping API contract. Generate/check types using task api:generate and task api:check.