Skip to content

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)

  1. One active mission globally -- Mission.is_active is a boolean flag. Only one satellite can be tracked across the entire network at any time.
  2. No organization concept -- Users exist but have no grouping. Any user can control any station.
  3. No concurrent tracking -- Station A and Station B must track the same satellite, even if they are on different continents with different sky coverage.
  4. No scheduling -- Missions are manually activated via the dashboard.
  5. 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:

  1. Every DB query includes org_id -- No global queries without org scope
  2. MQTT topics include org_slug -- Clean isolation per org
  3. Station IDs are globally unique -- Prevents collision across orgs
  4. SatelliteCache remains shared -- Orbital data is not org-specific
  5. Credentials are per-org -- Each org gets its own MQTT credentials