agmission/Documents/ARCHITECTURE.md

28 KiB

AgMission SaaS Platform Software Architecture

Version: 3.2.x
Last updated: April 2026
Author: AgNav Engineering


Table of Contents


1 Product Overview

AgMission is a cloud-based SaaS platform for precision aerial agriculture management. It serves agricultural aviation operators (applicators), their clients (growers/farmers), and integrated partners with console navigation aid systems (such as AgNav—Platinum and Titanium—and SatLoc).

The platform manages the complete lifecycle of an aerial application job:

  1. Mission planning — create jobs, define treatment areas, assign pilots and jobs to aircraft
  2. Job distribution — push assigned jobs to guidance console systems, in aircraft, on the field
  3. Real-time tracking — live GPS monitoring of aircraft during operations
  4. Post-flight processing — parse binary GPS/spray logs from console systems, compute coverage statistics
  5. Reporting and billing — generate application reports, manage invoices, and handle SaaS subscriptions

Supported languages: English, Portuguese (pt), Spanish (es)


2 System Components

Component Technology Purpose
Web Client Angular 9.1.13, NgRx 9, Leaflet 1.9, StimulSoftJS 2020.3.2 Browser-based management UI and report viewer
API Server Node.js 16.20.2 LTS, Express 4, Mongoose 6 Core REST API, business logic
GPS Server Node.js, TCP sockets Receives GPS data from AgNav (Platinum, Titanium) and RAP devices
Track Server Node.js, Express, SSE Real-time GPS tracking feed to browser
SatLoc Service Node.js Partner integration with SatLoc Cloud
Job Worker Node.js, RabbitMQ Async processing of uploaded job files
Invoice Worker Node.js, Cron Job Invoicing—manages applicator invoices for jobs performed
Partner Sync Worker Node.js, RabbitMQ Partner job upload and log processing
Partner Polling Worker Node.js, Cron Polls partner systems for new flight data
Cleanup Worker Node.js, Cron Purges soft-deleted data
Maintainer Node.js, Cron Scheduled database maintenance

3 High-Level Architecture

graph TD
    subgraph "Field Devices"
        AGN["AgNav Guidance<br/>System"]
        RAP["RAP Guidance<br/>System"]
        SLC["SatLoc Cloud<br/>Partner"]
    end

    subgraph "Browser"
        UI["Angular SPA<br/>(Web Client)"]
    end

    subgraph "AgMission Backend (agnav.com server)"
        NGINX["Nginx<br/>Reverse Proxy"]
        API["API Server<br/>Node.js / Express"]
        GPS["GPS Server<br/>TCP :6080 / :6082"]
        TRK["Track Server<br/>HTTP/SSE"]
        MNT["Maintainer<br/>Cron Service"]
        JW["Job Worker"]
        IW["Invoice Worker"]
        PSW["Partner Sync Worker"]
        PPW["Partner Polling Worker"]
        CW["Cleanup Worker"]
    end

    subgraph "Data Layer"
        MDB["MongoDB<br/>Replica Set"]
        RBT["RabbitMQ<br/>Message Broker"]
        RDS["Redis<br/>Cache"]
    end

    subgraph "External Services"
        STR["Stripe<br/>Billing"]
        SLC2["SatLoc Cloud API<br/>satloccloudfc.com"]
        SMTP["Email / SMTP"]
    end

    AGN -- "TCP :6080" --> GPS
    RAP -- "TCP :6082" --> GPS
    UI -- "HTTPS" --> NGINX
    NGINX -- "/" --> API
    NGINX -- "/track" --> TRK
    GPS -- "AMQP gdata queue" --> RBT
    RBT -- "consume gdata" --> TRK
    TRK -- "SSE" --> UI
    API -- "AMQP jobs / partner_tasks" --> RBT
    RBT -- "consume jobs" --> JW
    RBT -- "consume partner_tasks" --> PSW
    API --> MDB
    JW --> MDB
    IW --> MDB
    PSW --> MDB
    PPW --> MDB
    CW --> MDB
    MNT --> MDB
    API --> RDS
    JW --> RDS
    API --> STR
    PSW -- "REST" --> SLC2
    PPW -- "REST / cron" --> SLC2
    PPW -- "AMQP partner_tasks" --> RBT

4 Component Details

4-1 Web Client Angular SPA

Location: trunk/Development/client/
Version: 2.6.15

The frontend is a single-page application built with Angular 9 and served by Nginx. It communicates with the API Server over HTTPS and receives real-time GPS updates from the Track Server via Server-Sent Events (SSE).

Module structure

graph TD
    ROOT["AppModule<br/>(app.module.ts)"]
    ROOT --> AUTH["AuthModule<br/>Login, signup, password reset"]
    ROOT --> DASH["DashboardModule<br/>Overview metrics"]
    ROOT --> JOBS["JobModule<br/>Mission management<br/>Map editing, file upload"]
    ROOT --> TRACK["TrackModule<br/>Live GPS tracking map"]
    ROOT --> CUST["CustomerModule<br/>Client / grower management"]
    ROOT --> BILL["BillingModule<br/>Subscription and invoices"]
    ROOT --> INV["InvoicesModule<br/>Invoice listing and detail"]
    ROOT --> PART["PartnersModule<br/>Partner system users"]
    ROOT --> ADM["AdminModule<br/>Platform admin tools"]
    ROOT --> SET["SettingsModule<br/>User and account settings"]
    ROOT --> REP["ReportComponent<br/>PDF report viewer"]
    ROOT --> SIGN["SignupModule<br/>Self-service subscription signup"]

State management NgRx

The app uses NgRx (Redux pattern) for global state:

  • Store — single source of truth for session, entities, and UI state
  • Effects — side-effects (HTTP calls) triggered by dispatched actions
  • Entities — normalized collections (jobs, customers, pilots, vehicles, etc.)
  • Reducers — pure state transitions

Key features

Feature Description
Job map editor Leaflet map for defining treatment areas, waypoints, and obstacles
Live tracking Real-time aircraft position overlay via SSE
Data playback Replay completed flight paths from application logs
Invoicing Create, send, and track invoices
Subscription management Self-serve plan selection, trial, and upgrade; manage billing information; integrate with Stripe
Multi-language English / Portuguese / Spanish via Angular i18n
Partner customers View and manage SatLoc partner-linked customer accounts; support Satloc console systems, G4, Falcon in automated workflows

4-2 API Server

Location: trunk/Development/server/
Entry point: server.js
Port: AGM_PORT (default 7000 in production)

The central Express application. All browser requests pass through Nginx which proxies to this server. It handles authentication, all business logic REST endpoints, file uploads, Stripe webhooks, and report generation.

Request lifecycle

sequenceDiagram
    participant C as Client (Browser)
    participant N as Nginx
    participant S as API Server
    participant MW as Middlewares
    participant R as Routes
    participant CTL as Controller
    participant DB as MongoDB

    C->>N: HTTPS request
    N->>S: HTTP proxy (X-Forwarded-*)
    S->>MW: Rate limiter
    MW->>MW: JWT checkUser
    MW->>R: Router dispatch
    R->>CTL: Handler function
    CTL->>DB: Mongoose queries
    DB-->>CTL: Documents
    CTL-->>C: JSON response

Route groups

Route prefix Controller Description
/api/users user.js User account CRUD
/api/customers customer.js Applicator / client management
/api/jobs job.js Mission lifecycle
/api/upload upload_job.js Job file upload (ZIP/KML/SHP)
/api/pilots pilot.js Pilot management
/api/vehicles vehicle.js Aircraft management
/api/billing billing.js Stripe subscription billing
/api/subscription subscription.js Subscription plans
/api/invoices invoice.js Invoice management
/api/invoice_settings invoice_settings.js Invoice templates
/api/partners partner.js Partner org and system users
/api/dlq/:queue dlq.js Dead Letter Queue management
/api/export export.js Data export (CSV/IIF)
/api/v1 api_pub.js Public data export API (API-key auth)
/api/health health.js Health check endpoint
/stripe_webhooks subscription_webhooks.js Stripe event webhook

Key server-side helpers

Helper Purpose
helpers/constants.js Frozen enums (UserTypes, AppStatus, etc.)
helpers/env.js Typed environment variable access
helpers/subscription_util.js Stripe SDK wrappers
helpers/job_util.js Job state machine logic
helpers/geo_util.js Geospatial calculations (turf.js)
helpers/satloc_log_parser.js Binary SatLoc log file parser
helpers/satloc_application_processor.js Spray statistics from parsed logs
helpers/mailer.js Transactional email
helpers/logger.js Pino structured logging

4-3 GPS Server

Location: trunk/Development/gps-server/
Entry point: gps-server.js
Ports: TCP 6080 (AgNav), TCP 6082 (RAP)

A low-level TCP socket server that receives binary GPS telemetry from field console navigation aid systems (AgNav, RAP) in real-time.

How it works

sequenceDiagram
    participant D as AgNav / RAP Console System
    participant G as GPS Server (TCP)
    participant MDB as MongoDB
    participant RBT as RabbitMQ

    D->>G: TCP binary packet
    G->>G: Parse (AgNavParser / RAPParser)
    G->>MDB: Upsert location + location_cache
    G->>RBT: Publish to "gdata" queue

Two protocol variants are configured via PROTOCOL env var:

Protocol Port Device type
AGNAV 6080 AgNav console system (Platinum, Titanium)
RAP 6082 RAP binary protocol (external tracking devices)

The GPS Server writes every position to MongoDB (locations + location_cache collections) and simultaneously publishes to a RabbitMQ queue gdata, which the Track Server consumes for live SSE streaming.


4-4 Track Server

Location: trunk/Development/track-server/
Entry point: track-server.js
Protocol: HTTP/2 (via spdy) + Server-Sent Events

The Track Server bridges the real-time GPS queue to browser clients using SSE channels. Each client subscribes to one or more vehicle channels; the server pushes position updates as they arrive from RabbitMQ.

sequenceDiagram
    participant RBT as RabbitMQ gdata
    participant TS as Track Server
    participant BR as Browser (SSE client)

    RBT-->>TS: GPS data message
    TS->>TS: setVehGpsData(data)
    TS->>BR: SSE event (vehicle position)

Authentication is JWT-based — clients obtain a short-lived track token from the API Server, then connect to Track Server with it.


4-5 SatLoc Integration Service

Location: trunk/Development/satloc/
Purpose: Batch job of importing completed flight logs from the SatLoc Cloud partner system.

SatLoc is an aerial guidance hardware vendor. When an applicator uses SatLoc hardware, their flight logs are stored in SatLoc's cloud. This service:

  1. Authenticates with SatLoc Cloud (satloccloudfc.com)
  2. Retrieves aircraft log metadata
  3. Downloads binary log files
  4. Parses the proprietary binary format
  5. Creates ApplicationDetail records in AgMission's database

The core C# parsing logic (frmMain.cs, APIObjects.cs) is the original reference implementation; the production path is the Node.js port in satloc-api.js and the server-side helpers/satloc_log_parser.js.


4-6 Background Workers

All workers are independent Node.js processes managed by PM2. They communicate via RabbitMQ queues and share the same MongoDB instance as the API Server.

Worker overview

graph LR
    API["API Server"] -- "publish jobs" --> JQ["jobs queue<br/>(RabbitMQ)"]
    API -- "publish partner_tasks" --> PQ["partner_tasks queue<br/>(RabbitMQ)"]
    JQ --> JW["Job Worker<br/>job_worker.js"]
    PQ --> PSW["Partner Sync Worker<br/>partner_sync_worker.js"]
    PPW["Partner Polling Worker<br/>(cron every 15 min)"] -- "publish partner_tasks" --> PQ
    PPW -- "REST poll" --> SLCAPI["SatLoc Cloud API"]
    IW["Invoice Worker<br/>(cron every 1 min)"] --> MDB["MongoDB"]
    CW["Cleanup Worker<br/>(cron weekly)"] --> MDB
    JW --> MDB
    PSW --> MDB
    PSW -- "REST" --> SLCAPI

Job Worker

Consumes the jobs RabbitMQ queue. Triggered when a user uploads a job file via the web UI.

Responsibilities:

  • Extract ZIP archives containing job data files
  • Parse AgNav binary (.agn), KML, and Shapefile formats
  • Calculate sprayed area, coverage geometry, application statistics
  • Create Application, ApplicationFile, and ApplicationDetail records
  • Use Redis for deduplication and temporary state

Invoice Worker

Cron-driven (every minute). Manages Job Invoicing—the lifecycle of applicator invoices for jobs performed on behalf of clients. Handles invoice state transitions and late-payment notifications.

Note: Job Invoicing is separate from SaaS subscription management (which is handled by Stripe webhooks in the API Server).

stateDiagram-v2
    direction LR
    [*] --> Draft : invoice created
    Draft --> Open : openDate reached
    Open --> Overdue : dueDate passed
    Open --> Paid : payment received
    Overdue --> Paid : late payment
    Paid --> [*]

Partner Sync Worker

Consumes the partner_tasks queue. Handles two task types:

Task type Action
UPLOAD_PARTNER_JOB Pushes a job (waypoints, boundaries) to SatLoc Cloud for the assigned aircraft
PROCESS_PARTNER_LOG Parses a downloaded SatLoc binary log file and creates ApplicationDetail records

Partner Data Polling Worker

Cron-driven (every 15 min in production, every 1 min in development).

graph TD
    A["Cron trigger"] --> B["Find JobAssigns<br/>status = UPLOADED"]
    B --> C["Group by partner + customer"]
    C --> D["Call SatLoc: GetAircraftLogs"]
    D --> E{"New log<br/>files?"}
    E -- No --> F["Done"]
    E -- Yes --> G["Download log file<br/>to local storage"]
    G --> H["Create PartnerLogTracker<br/>PENDING → DOWNLOADED"]
    H --> I["Enqueue PROCESS_PARTNER_LOG<br/>to partner_tasks queue"]

Cleanup Worker

Weekly cron job. Hard-deletes records that have been soft-deleted (markedDelete: true) and older than a retention period. Applies to customers, jobs, pilots, vehicles, and related entities.


4-7 Maintainer Service

Location: trunk/Development/maintainer/
Entry point: index.js

A lightweight cron-based utility for database maintenance tasks not suitable for the main server. Scheduled tasks:

Task Schedule (prod) Description
cleanMarkedDeleteData Weekly (Sunday 01:00 UTC) Remove soft-deleted customer records

Connects to MongoDB using the same models as the API Server (shared model/ layer).


5 Data Architecture

MongoDB collections

erDiagram
    USER {
        ObjectId _id
        string kind
        string email
        string name
        string userType
        boolean active
        ObjectId parent
    }
    CUSTOMER {
        ObjectId _id
        string name
        ObjectId byPuid
    }
    JOB {
        number _id
        string name
        ObjectId customer
        ObjectId byPuid
        number status
        Date startDate
    }
    JOB_ASSIGN {
        ObjectId _id
        number job
        ObjectId user
        number status
        string extJobId
    }
    APPLICATION {
        ObjectId _id
        number jobId
        string fileName
        Date startDateTime
        Date endDateTime
        number totalSprayed
    }
    APPLICATION_DETAIL {
        ObjectId _id
        ObjectId appId
        number lat
        number lon
        number rate
        Date gdt
    }
    SUBSCRIPTION {
        ObjectId _id
        ObjectId byPuid
        string stripeSubId
        string planKey
        string status
    }
    INVOICE {
        ObjectId _id
        ObjectId byPuid
        number status
        Date openDate
        Date dueDate
    }
    PARTNER_LOG_TRACKER {
        ObjectId _id
        string status
        ObjectId jobAssignId
        string localFilePath
    }

    USER ||--o{ JOB : "creates (byPuid)"
    CUSTOMER ||--o{ JOB : "associated"
    JOB ||--o{ JOB_ASSIGN : "assigned to"
    JOB_ASSIGN }o--|| USER : "pilot/device"
    JOB ||--o{ APPLICATION : "contains"
    APPLICATION ||--o{ APPLICATION_DETAIL : "detail records"
    USER ||--o{ SUBSCRIPTION : "holds"
    USER ||--o{ INVOICE : "receives"
    JOB_ASSIGN ||--o{ PARTNER_LOG_TRACKER : "tracks"

User type hierarchy

The User model uses a Mongoose discriminator pattern to represent multiple actor types from one collection:

userType code Kind Description
0 ADMIN Platform administrator
1 APP Applicator (main operator account)
2 APP_ADM Applicator admin
3 CLIENT Client / grower (read-only)
4 OFFICER Field officer
5 PILOT Pilot
6 INSPECTOR Inspector
9 DEVICE Aircraft / guidance unit
20 PARTNER Partner organization (e.g., SatLoc)
21 PARTNER_SYSTEM_USER Customer account in partner system

6 Message Queue Architecture

RabbitMQ is used for all async work. Queue names are auto-prefixed with dev_ in non-production environments.

graph LR
    subgraph "Producers"
        API["API Server"]
        PPW["Partner Polling Worker"]
        GPS["GPS Server"]
    end

    subgraph "Queues (RabbitMQ)"
        JQ["jobs"]
        PQ["partner_tasks"]
        PDLQ["partner_tasks_failed<br/>(DLQ)"]
        GQ["gdata"]
    end

    subgraph "Consumers"
        JW["Job Worker"]
        PSW["Partner Sync Worker"]
        TRK["Track Server"]
    end

    API --> JQ
    API --> PQ
    PPW --> PQ
    GPS --> GQ
    JQ --> JW
    PQ --> PSW
    PSW -- "on max retries" --> PDLQ
    GQ --> TRK

The Dead Letter Queue (partner_tasks_failed) is managed through the /api/dlq/:queueName/* API endpoints, which provide list, retry, and purge operations.


7 Partner Integration Architecture

sequenceDiagram
    participant UI as Web Client
    participant API as API Server
    participant PSW as Partner Sync Worker
    participant PPW as Partner Polling Worker
    participant SLC as SatLoc Cloud

    UI->>API: Assign job to SatLoc device
    API->>API: Create JobAssign (status=NEW)
    API->>PSW: Enqueue UPLOAD_PARTNER_JOB
    PSW->>SLC: POST /api/Satloc/UploadJobData
    SLC-->>PSW: extJobId
    PSW->>API: Update JobAssign (status=UPLOADED, extJobId)

    Note over PPW: Cron: every 15 min
    PPW->>API: Find JobAssigns status=UPLOADED
    PPW->>SLC: GET /api/Satloc/GetAircraftLogs
    SLC-->>PPW: Log list
    PPW->>SLC: GET /api/Satloc/GetAircraftLogData
    SLC-->>PPW: Binary log file
    PPW->>PPW: Store file locally (SATLOC_STORAGE_PATH)
    PPW->>API: Create PartnerLogTracker (DOWNLOADED)
    PPW->>PSW: Enqueue PROCESS_PARTNER_LOG
    PSW->>PSW: Parse binary log (SatLocLogParser)
    PSW->>API: Create ApplicationDetail records

Partner credential model

Each customer that uses a SatLoc device has a dedicated PartnerSystemUser record (userType=21) that stores their SatLoc companyId, partnerUserId, and API key. This isolates customer data within the partner system.


8 Billing and Subscription Architecture

sequenceDiagram
    participant UI as Web Client
    participant API as API Server
    participant STR as Stripe

    UI->>API: Select subscription plan
    API->>STR: Create SetupIntent or Subscription
    STR-->>API: Client secret
    UI->>STR: Confirm card (3DS if needed)
    STR->>API: POST /stripe_webhooks (subscription events)
    API->>API: Update Subscription record

Note: SaaS subscription management (tiers, billing cycles) is handled through Stripe webhooks. Job Invoicing (applicators invoicing clients for jobs performed) is a separate function managed by the Invoice Worker.

SaaS Subscription tiers (mapped to Stripe price IDs via env vars):

Tier Env key prefix Description
Essential ESS_1ESS_5 Entry-level operator plans
Enterprise ENT_1ENT_4 High-volume operator plans
Add-on ADDON_1 Additional features, refer to Live Tracking service

9 Authentication and Authorization

JWT authentication

All API Server endpoints (except signup and Stripe webhooks) require a JWT Bearer token:

Authorization: Bearer <token>

Tokens are issued on login and carry userType, byPuid (applicator ID), and userId. The checkUser middleware validates the token and attaches the user to req.user.

API key authentication Public Export API

The /api/v1/ public export endpoints use an X-API-Key header instead of JWT. Keys are bcrypt-hashed and stored in the ApiKey collection. Each key is scoped to a specific applicator (byPuid), so data access is automatically isolated.

Authorization model

ADMIN       — full platform access
APP         — manages own organization (pilots, vehicles, jobs, customers)
APP_ADM     — same as APP within parent organization
CLIENT      — read-only access to own job results
PILOT       — limited: download job assignments
OFFICER     — field oversight
INSPECTOR   — read-only job inspection
PARTNER     — manages partner organization
PARTNER_SYSTEM_USER — customer credentials for a specific partner

10 Infrastructure and Deployment

Production server topology

graph TD
    INT["Internet"] --> NX["Nginx<br/>SSL termination<br/>port 443"]
    NX -- "/ → :7000" --> API["agmission-prod<br/>(PM2)"]
    NX -- "/track → :4200" --> TRK["track_server<br/>(PM2)"]
    API --> MDB["MongoDB<br/>Replica Set<br/>rs0"]
    API --> RBT["RabbitMQ"]
    API --> RDS["Redis"]
    GPS1["gps_server-agnav<br/>TCP :6080 (PM2)"] --> MDB
    GPS1 --> RBT
    GPS2["gps_server-rap<br/>TCP :6082 (PM2)"] --> MDB
    GPS2 --> RBT
    RBT --> JW["job_worker<br/>(PM2)"]
    RBT --> PSW["partner_sync_worker<br/>(PM2)"]
    PPW["partner_data_polling_worker<br/>(PM2)"] --> MDB
    PPW --> RBT
    IW["invoice_worker<br/>(PM2)"] --> MDB
    CW["cleanup_worker<br/>(PM2)"] --> MDB

PM2 managed processes

PM2 name Entry point Description
agmission-prod server.js Main API server
track_server track-server.js Live tracking
gps_server-agnav gps-server.js AgNav TCP receiver
gps_server-rap gps-server.js RAP TCP receiver
job_worker / job-importer workers/job_worker.js Job file processing
invoice_worker workers/invoice_worker.js Invoice automation
cleanup_worker workers/cleanup_worker.js Soft-delete cleanup
partner_sync_worker workers/partner_sync_worker.js Partner job/log sync
partner_data_polling_worker workers/partner_data_polling_worker.js Partner log polling

Deployment flow

Deployments are performed using trunk/Others/scripts/deploy/agm-deploy.sh. See DEPLOYMENT.md for full instructions.

graph LR
    DEV["Local dev machine<br/>(SVN trunk or branch)"] -- "rsync over SSH<br/>agm-deploy.sh" --> PROD["Production server<br/>agmission-1.agnav.com:22222"]
    PROD --> PM2["pm2 reload<br/>agmission-prod"]

11 Key Data Flows

Job creation and assignment

graph TD
    A["Applicator creates job<br/>in Web UI"] --> B["POST /api/jobs"]
    B --> C["Job record created<br/>in MongoDB"]
    C --> D["Upload job file<br/>(ZIP / KML / SHP)"]
    D --> E["POST /api/upload"]
    E --> F["File stored on disk<br/>job-uploads/"]
    F --> G["Job message published<br/>to 'jobs' queue"]
    G --> H["Job Worker consumes<br/>message"]
    H --> I["Unzip and parse files"]
    I --> J["Calculate spray statistics"]
    J --> K["Create Application +<br/>ApplicationDetail records"]
    K --> L["Applicator assigns job<br/>to pilot / device / partner"]
    L --> M{"Partner?"}
    M -- "No (internal)" --> N["JobAssign created<br/>status=NEW"]
    M -- "Yes (SatLoc)" --> O["UPLOAD_PARTNER_JOB<br/>enqueued"]
    O --> P["Partner Sync Worker<br/>uploads to SatLoc Cloud"]
    P --> Q["JobAssign status=UPLOADED<br/>+ extJobId stored"]

Real-time GPS tracking

graph LR
    HW["AgNav Device"] -- "binary TCP" --> GPS["GPS Server"]
    GPS --> MDB["MongoDB<br/>locations"]
    GPS --> RBT["RabbitMQ gdata"]
    RBT --> TRK["Track Server"]
    TRK -- "SSE push" --> UI["Browser Map"]

12 Repository Layout

AgMission/
├── trunk/
│   ├── Development/          ← Active source code (see below)
│   ├── Documents/            ← Architecture, requirements, design docs
│   └── Others/
│       ├── configs/          ← PM2 JSON configs for deployment target
│       └── scripts/          ← Operations scripts
│           ├── deploy/       ← Deployment automation (agm-deploy.sh)
│           ├── backup_agm.sh ← MongoDB backup + rsync to NAS
│           └── start_pm2_apps.sh
│
├── branches/                 ← Feature branches (SVN layout)
│   ├── subscription-invoicing/
│   ├── subscription-signup/
│   ├── job-invoicing/
│   ├── data-export-api/
│   └── satloc-resume/
│
└── tags/                     ← Release snapshots
    └── release-3.2.1/

trunk/Development/
├── client/        Angular SPA
├── server/        Express API server + workers
│   ├── controllers/
│   ├── helpers/
│   ├── middlewares/
│   ├── model/
│   ├── routes/
│   ├── services/
│   └── workers/
├── gps-server/    TCP GPS receiver
├── track-server/  SSE live tracking
├── satloc/        SatLoc integration (batch import)
├── maintainer/    Scheduled DB maintenance
├── shared/        Shared DB utilities and models
└── libs/          Bundled third-party libs (shapefile, etc.)

For deployment instructions see DEPLOYMENT.md.
For the server-side REST API reference see server/docs/API_SPECIFICATION.md.
For partner integration details see server/docs/PARTNER_INTEGRATION_ARCHITECTURE.md.