0004 - Jurisdiction Service
PRD: Jurisdiction Service (MVP)
| Field | Value |
|---|---|
| Status | Draft |
| Owner | TBD |
| Contributors | TBD |
| Date | 2026-04-01 |
1. Executive Summary
Problem Statement: DroneUp has no authoritative service defining who is responsible for managing a given volume of airspace, nor what operational rules apply within it. Without this, downstream services (authorization, flight planning) have no structured source of truth for airspace governance and zone-based rules.
Proposed Solution: A standalone Go microservice exposing a REST API for CRUD management of named Jurisdictions and their sub-Zones. Each Jurisdiction owns one or more 2.5D volumetric boundaries (polygon + AGL floor/ceiling). Each Zone is a typed operational polygon constrained within a parent jurisdiction’s volume, stored in PostGIS 16+.
Success Criteria:
- A jurisdiction with multiple volumes can be created, retrieved, updated, and deleted via REST API running locally.
- A zone can be created within a jurisdiction and is validated to fall within a jurisdiction volume’s 2D footprint and altitude band.
- JurisdictionVolume and Zone geometry is stored and returned as valid GeoJSON polygons.
- Floor/ceiling values are stored in feet AGL and returned accurately.
- Service starts cleanly with
docker compose upagainst a local PostGIS instance. - API returns appropriate HTTP status codes and error bodies for invalid inputs.
Scope Clarity: A — requirements are clear and bounded for MVP.
2. User Experience & Functionality
User Personas
- *Authority — Define airspace jurisdictions and zones.
- Authorization service (future, machine consumer) — will query jurisdiction/zone data to make airspace access decisions.
User Stories & Acceptance Criteria
US-01: Create a jurisdiction
As an authority operator, I want to create a named jurisdiction with one or more volumetric polygons so that I can define who manages a region of airspace.
POST /v1/jurisdictionsacceptsname(string, required),is_active(boolean, optional, defaultstrue), andvolumes(array, min 1, required).- Each volume contains a GeoJSON polygon,
floor_value(float, ≥ 0),floor_unit(ft),floor_ref(AGL),ceiling_value(float, >floor_value),ceiling_unit(ft),ceiling_ref(AGL). - Returns
201 Createdwith full resource including generatedidfields. - Returns
400 Bad Requestifnamemissing,volumesempty, polygon invalid GeoJSON, ceiling ≤ floor, or unit/ref values are not in allowed enum.
US-02: Retrieve a jurisdiction
As an authority operator, I want to fetch a jurisdiction by ID so that I can review its configuration.
GET /v1/jurisdictions/{id}returns the jurisdiction with all its volumes.- Returns
404 Not Foundif ID does not exist.
US-03: List all jurisdictions
As a droneup operator, I want to list all jurisdictions so that I can see what is currently defined.
GET /v1/jurisdictionsreturns an array of all jurisdictions with their volumes.- Returns empty array (not 404) when none exist.
US-04: Update a jurisdiction
As an authority operator, I want to update a jurisdiction’s name, active status, or volumes.
PUT /v1/jurisdictions/{id}replacesname,is_active, andvolumesarray entirely.- Returns
200 OKwith updated resource. - Returns
404 Not Foundif ID does not exist. Same validation rules as creation apply.
US-05: Delete a jurisdiction
As an authority operator, I want to delete a jurisdiction and all its volumes and zones.
DELETE /v1/jurisdictions/{id}removes the jurisdiction, all its volumes, and all its zones (cascade).- Returns
204 No Contenton success. - Returns
404 Not Foundif ID does not exist.
US-06: Create a zone
As an authority operator, I want to create a typed zone within a jurisdiction so that I can define operational rules for a sub-region of airspace.
POST /v1/zonesacceptsjurisdiction_id(UUID, required),type(no-fly|autoapprove, required),polygon(GeoJSON, required), and optionallyfloor_value/floor_unit/floor_ref/ceiling_value/ceiling_unit/ceiling_ref.- If altitude fields are omitted, they default to the floor/ceiling of the matching parent jurisdiction volume.
- Validation on insert:
- Zone polygon must be geometrically contained within (
ST_CoveredBy) the 2D footprint of at least one of the jurisdiction’s volumes. - Zone
floor_valuemust be ≥ matched volume’sfloor_value(same unit/ref). - Zone
ceiling_valuemust be ≤ matched volume’sceiling_value(same unit/ref).
- Zone polygon must be geometrically contained within (
- Returns
201 Createdwith full resource. - Returns
400 Bad Requestif jurisdiction not found, polygon falls outside all volumes, altitude band violates parent volume constraints, or unit/ref values are not in allowed enum.
US-07: Retrieve / list zones
As a platform operator, I want to view zones for a jurisdiction.
GET /v1/zones?jurisdiction_id={id}returns all zones for a jurisdiction.GET /v1/zones/{id}returns a single zone.- Returns empty array when no zones exist for the jurisdiction.
US-08: Update a zone
As an authority operator, I want to update a zone’s geometry, altitude, or type.
PUT /v1/zones/{id}replaces the zone. Same validation rules as creation apply.- Returns
200 OKwith updated resource. - Returns
404 Not Foundif ID does not exist.
US-09: Delete a zone
As an authority operator, I want to delete a zone.
DELETE /v1/zones/{id}removes the zone.- Returns
204 No Contenton success. - Returns
404 Not Foundif ID does not exist.
Non-Goals (MVP)
| Out of Scope | Rationale |
|---|---|
| Authentication / authorization on the API | MVP — deferred |
| Spatial intersection / query endpoints | Not needed until authorization service is built |
| Overlap enforcement between zones or volumes | Deferred |
| Bulk import / export | Not required for MVP |
| Audit trail / change history | Deferred |
| Effective date ranges | Deferred |
| Additional jurisdiction/zone metadata fields | Deferred — name + type only for MVP |
| Pagination on list endpoints | Deferred — low volume at MVP |
MVT tile endpoint (/v1/zones/{z}/{x}/{y}) | v1.1 — deferred |
Server-side enforcement of is_active flag | Consumer responsibility — not enforced by this service in MVP |
| Altitude unit/ref normalization across mismatched values | v1.1 — MVP enforces ft / AGL only |
3. Technical Specifications
Architecture Overview
Apollo Frontend
│
▼ REST (HTTP/JSON)
jurisdiction-service (Go)
│ ├─ /v1/jurisdictions (CRUD)
│ └─ /v1/zones (CRUD + future MVT)
▼
PostGIS 16+ (PostgreSQL)
│
[future]
▼
authorization-service (reads jurisdiction + zone data)Tech Stack
- Language: Go
- Database: PostgreSQL 16 + PostGIS extension
- Geometry storage: PostGIS
geometry(Polygon, 4326)— WGS84 - API format: REST / JSON, GeoJSON for polygon representation
- Local dev: Docker Compose (service + PostGIS container)
API Surface
| Method | Path | Description |
|---|---|---|
POST | /v1/jurisdictions | Create jurisdiction + volumes |
GET | /v1/jurisdictions | List all jurisdictions |
GET | /v1/jurisdictions/{id} | Get jurisdiction by ID |
PUT | /v1/jurisdictions/{id} | Replace jurisdiction + volumes |
DELETE | /v1/jurisdictions/{id} | Delete jurisdiction, volumes, and zones |
POST | /v1/zones | Create zone (validated against parent volume) |
GET | /v1/zones | List zones (filter by jurisdiction_id) |
GET | /v1/zones/{id} | Get zone by ID |
PUT | /v1/zones/{id} | Replace zone |
DELETE | /v1/zones/{id} | Delete zone |
GET | /healthz | Health check |
Data Model
jurisdictions
| Column | Type | Notes |
|---|---|---|
id | UUID | PK, generated |
name | TEXT | NOT NULL |
is_active | BOOLEAN | NOT NULL, default true |
created_at | TIMESTAMPTZ | NOT NULL, default now() |
updated_at | TIMESTAMPTZ | NOT NULL, default now() |
jurisdiction_volumes
| Column | Type | Notes |
|---|---|---|
id | UUID | PK, generated |
jurisdiction_id | UUID | FK → jurisdictions(id) ON DELETE CASCADE |
polygon | geometry(Polygon, 4326) | NOT NULL |
floor_value | FLOAT8 | NOT NULL, ≥ 0 |
floor_unit | TEXT | NOT NULL, default ft — enum: ft |
floor_ref | TEXT | NOT NULL, default AGL — enum: AGL |
ceiling_value | FLOAT8 | NOT NULL, > floor_value |
ceiling_unit | TEXT | NOT NULL, default ft — enum: ft |
ceiling_ref | TEXT | NOT NULL, default AGL — enum: AGL |
created_at | TIMESTAMPTZ | NOT NULL, default now() |
zones
| Column | Type | Notes |
|---|---|---|
id | UUID | PK, generated |
jurisdiction_id | UUID | FK → jurisdictions(id) ON DELETE CASCADE |
polygon | geometry(Polygon, 4326) | NOT NULL |
floor_value | FLOAT8 | NOT NULL, ≥ 0 |
floor_unit | TEXT | NOT NULL, default ft — enum: ft |
floor_ref | TEXT | NOT NULL, default AGL — enum: AGL |
ceiling_value | FLOAT8 | NOT NULL, > floor_value |
ceiling_unit | TEXT | NOT NULL, default ft — enum: ft |
ceiling_ref | TEXT | NOT NULL, default AGL — enum: AGL |
type | TEXT | NOT NULL — enum: no-fly | autoapprove |
created_at | TIMESTAMPTZ | NOT NULL, default now() |
updated_at | TIMESTAMPTZ | NOT NULL, default now() |
Indexes:
jurisdiction_volumes(jurisdiction_id)— FK lookupjurisdiction_volumesGIST index onpolygon— spatial validation + future querieszones(jurisdiction_id)— FK lookup + filterzonesGIST index onpolygon— spatial validation + future MVT queries
Zone Altitude Defaulting Logic
On POST /v1/zones, if altitude fields are omitted:
- Find the jurisdiction volume whose 2D polygon contains (
ST_CoveredBy) the zone polygon. - Copy all 6 altitude columns (
floor_value,floor_unit,floor_ref,ceiling_value,ceiling_unit,ceiling_ref) from that volume as the zone’s defaults. - If no single volume contains the zone polygon, return
400— zone must fall within one volume.
Altitude Validation Rules (all endpoints)
floor_unitandceiling_unitmust beft— return400otherwisefloor_refandceiling_refmust beAGL— return400otherwiseceiling_value>floor_value— return400otherwisefloor_value≥ 0 — return400otherwise
v1.1 note: additional enum values (
m,MSL, etc.) will be added when cross-unit normalization logic is implemented.
Integration Points
- Apollo frontend: REST consumer — UI for managing jurisdictions and zones
- authorization-service (future): will read jurisdiction + zone data; interface TBD at that service’s design time
Security & Privacy
- No auth on API for MVP — must be addressed before any non-local deployment
- No PII in data model
- PostGIS must not be exposed outside the local Docker network
5. Risks & Phased Rollout
Technical Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| GeoJSON ↔ PostGIS edge cases (invalid rings, winding order) | Medium | Medium | Validate with ST_IsValid server-side; return descriptive errors |
| Zone altitude defaulting selects wrong volume if jurisdiction has overlapping volumes | Low | Medium | Document tie-breaking rule (first matched volume by insertion order); revisit with overlap enforcement in v1.1 |
| No auth — accidental API exposure | Low (local MVP) | High | Document prominently; enforce NetworkPolicy when deployed to cluster |
PUT full-replace on jurisdiction cascades to zones — caller must re-submit zones | Medium | Medium | Document clearly; PATCH support added in v1.1 |
Phased Rollout
MVP (current)
- CRUD for jurisdictions, volumes, and zones
- Zone validation against parent volume (2D containment + altitude band)
- Altitude represented as
floor_value/floor_unit/floor_ref— validated enum (ft/AGL) - No auth, local PostGIS, Docker Compose
- Zone types:
no-fly,autoapprove
Dependencies
- PostGIS 16+ Docker image for local dev
- authorization-service design will determine query interface into this service
6. Estimation Input
prd_sizing_input:
feature: "jurisdiction-service MVP"
scope_clarity: "A"
key_terms:
- "jurisdiction"
- "airspace volume"
- "zone"
- "postgis"
- "geospatial"
- "no-fly"
risk_flags:
- "new-service"
- "geospatial-data"
- "spatial-validation"
affected_repos:
- "jurisdiction-service"
domains:
- "backend-go"
- "infrastructure-docker"
regulatory: false
discovery_needed: false