TALOS v0.2 Development Strategy: Organization-Grade Mission Control¶
Vision¶
Transform TALOS from a single-operator, single-mission tool into an organization-grade ground station network controller that supports concurrent multi-satellite tracking, role-based access, and campaign-based scheduling -- while designing the data model to support future multi-tenancy.
Current Limitations (v0.1)¶
- One active mission globally --
Mission.is_activeis a boolean flag. Only one satellite can be tracked across the entire network at any time. - No organization concept -- Users exist but have no grouping. Any user can control any station.
- No concurrent tracking -- Station A and Station B must track the same satellite, even if they are on different continents with different sky coverage.
- No scheduling -- Missions are manually activated via the dashboard.
- No role-based access -- Every authenticated user has full admin privileges.
Target Architecture (v0.2)¶
Data Model¶
Organization
|
+-- Members (User + Role)
| roles: owner, operator, viewer
|
+-- Stations (owned by org, operated by members)
|
+-- Campaigns (replaces Mission)
| |-- Target satellite (NORAD ID, SatNOGS ID)
| |-- Priority (1-10)
| |-- Status: draft / scheduled / active / completed / cancelled
| |-- Transmitter selection
| |
| +-- Assignments (which stations, when)
| |-- station_id
| |-- window_start, window_end (UTC)
| |-- status: pending / tracking / completed
|
+-- SatelliteCache (shared, not org-scoped)
Key Schema Changes¶
-- NEW: Organization
CREATE TABLE organization (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
slug VARCHAR UNIQUE NOT NULL, -- URL-safe identifier
created_at TIMESTAMP DEFAULT now()
);
-- NEW: Membership (User <-> Organization with role)
CREATE TABLE membership (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES "user"(id),
org_id INT REFERENCES organization(id),
role VARCHAR NOT NULL DEFAULT 'operator', -- owner, operator, viewer
created_at TIMESTAMP DEFAULT now(),
UNIQUE(user_id, org_id)
);
-- MODIFIED: Station gets org_id (replaces owner_email)
ALTER TABLE station ADD COLUMN org_id INT REFERENCES organization(id);
ALTER TABLE station DROP COLUMN owner_email;
-- NEW: Campaign (replaces Mission)
CREATE TABLE campaign (
id SERIAL PRIMARY KEY,
org_id INT REFERENCES organization(id),
name VARCHAR NOT NULL,
satnogs_id VARCHAR,
norad_id INT NOT NULL,
priority INT DEFAULT 5,
status VARCHAR DEFAULT 'draft', -- draft/scheduled/active/completed/cancelled
transmitter_uuid VARCHAR, -- selected transmitter (nullable = auto)
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
-- NEW: Assignment (Campaign <-> Station time window)
CREATE TABLE assignment (
id SERIAL PRIMARY KEY,
campaign_id INT REFERENCES campaign(id),
station_id INT REFERENCES station(id),
window_start TIMESTAMP NOT NULL,
window_end TIMESTAMP NOT NULL,
status VARCHAR DEFAULT 'pending', -- pending/tracking/completed/failed
created_at TIMESTAMP DEFAULT now(),
UNIQUE(station_id, window_start) -- no double-booking
);
MQTT Topic Evolution¶
Current: talos/gs/{station_id}/cmd/rot
Future: talos/{org_slug}/gs/{station_id}/cmd/rot
This scopes all MQTT traffic by organization, enabling: - Per-org MQTT ACLs - Future multi-tenancy on a shared broker - Clean topic partitioning
Director Evolution¶
Current: One loop, one mission, all stations. Future: One loop, multiple campaigns, per-station assignments.
# Current (simplified)
for station in all_stations:
az, el = compute(current_satellite, station)
publish(f"talos/gs/{sid}/cmd/rot", {az, el})
# Future
for assignment in active_assignments:
campaign = assignment.campaign
station = assignment.station
satellite = get_satellite(campaign)
az, el = compute(satellite, station)
publish(f"talos/{org}/gs/{sid}/cmd/rot", {az, el})
API Evolution¶
All endpoints become org-scoped:
GET /api/org/{slug}/stations -- List org stations
POST /api/org/{slug}/stations -- Provision station
GET /api/org/{slug}/campaigns -- List campaigns
POST /api/org/{slug}/campaigns -- Create campaign
POST /api/org/{slug}/campaigns/{id}/schedule -- Auto-schedule
GET /api/org/{slug}/campaigns/{id}/assignments
POST /api/org/{slug}/campaigns/{id}/activate
Backward compatibility: If no org exists, auto-create a "default" org for the user.
RBAC¶
| Role | Can do |
|---|---|
| owner | Everything + manage members + delete org |
| operator | Manage stations + campaigns + activate tracking |
| viewer | Read-only dashboard access |
Implementation Phases¶
Phase 1: Data Model (this sprint)¶
- Alembic migrations for new schema
- Organization, Membership, Campaign, Assignment models
- Backward-compatible: auto-migrate existing data to default org
Phase 2: Director Multi-Campaign¶
- Director reads active assignments instead of global is_active
- Per-station satellite tracking (different stations, different targets)
- Campaign lifecycle (pending -> tracking -> completed)
Phase 3: API + RBAC¶
- Org-scoped endpoints
- Role checking middleware
- Campaign CRUD
- Assignment CRUD
Phase 4: Scheduling Engine¶
- Auto-assign stations to campaigns based on pass windows
- Constraint solver (OR-Tools): maximize coverage, respect priorities
- Conflict detection (no double-booking)
Phase 5: Dashboard¶
- Multi-campaign view (track N satellites simultaneously)
- Station assignment timeline
- Org management UI
- Role-based UI (viewers see less)
Phase 6: MQTT + shared/ Updates¶
- Org-scoped topics
- Updated schemas for campaigns and assignments
- Updated ACLs
Design for Multi-Tenancy (Model C)¶
While not implementing full multi-tenancy now, every design decision should support it later:
- Every DB query includes org_id -- No global queries without org scope
- MQTT topics include org_slug -- Clean isolation per org
- Station IDs are globally unique -- Prevents collision across orgs
- SatelliteCache remains shared -- Orbital data is not org-specific
- Credentials are per-org -- Each org gets its own MQTT credentials