DroneService Pro Roadmap
Edit modules and tools

Texas Drone Company · DroneService Pro

DroneService Pro Roadmap

What the platform is, and the roadmap for building it out of the back office, the client portal, and the field tools. Written so a new reader can see the shape in a page and a developer can see the order of events.

Living roadmapUpdated September 6, 2026Companion to the DroneVision Operator Model

In one paragraph

DroneService Pro is one platform for drone service work. The paying tenant is an operator, a drone service provider, with its staff and its client companies underneath. Each company is a tree of whatever its industry calls the places or engagements it works on, sites or projects or fields, and work orders hang under those. What an operator buys are modules, bundles for one type of work such as Construction or Survey and Mapping or the Business back office. What we build are tools, each written once and shared by whichever modules include it. Underneath every tool sit the same records, the same API, the same event log, and one serverless kitchen for the heavy work.

How this page came to be, and how to read it

Two production apps exist today: DSP, the back office, and DroneVision, the client portal. Neither serves more than one operator. Both need the same new layer above them, and that layer plus what surrounds it is the platform. So nobody folds under anybody: DSP contributes the product, DroneVision contributes the plumbing, and the result is named DroneService Pro.

Ten minutes: read the paragraph above, section 1, and the stage timeline in section 8. Building it: read Part 1 in full before Part 2; the tables carry the numbers, the diagrams carry the mechanisms. Keeping it current: the registry at the end is live. Names, modules, tool assignments, and stage status edited there flow through everything above.

Part 1

The platform

What DroneService Pro is once it exists. Read this before the roadmap.

1Platform, tools, modules

The thing you sell and the thing you build are different levels, and keeping them apart is what lets new industries arrive without forking anything.

  • Platform. DroneService Pro. The records every tool shares: Operator, Company as a tree, Work Order, Asset, Person. Billing, permissions, events, and the API. Nothing industry-specific lives here.
  • Tools. The unit you build. A tool owns its tables, API, screens, permissions, and events, and is written once. A map viewer, a plan overlay, a GCP planner, a chemical inventory, a work order system.
  • Modules. The unit you sell. A module is a curated bundle of tools plus the vocabulary, defaults, templates, and workflows for one type of work. Mostly data, not code. AeroRev already works this way with its industry packs.
ONE TOOL, TWO MODULES CONSTRUCTION · MODULE nodes are "Projects" · work orders are "Captures" · delivery hub SURVEY AND MAPPING · MODULE nodes are "Projects" · work orders are "Sessions" · CRS defaults Photo Galleries Ground Capture Progress Timeline Plan Overlays Share Links GCP Planner Mission QC Point Clouds 3D Captures Layouts and Print Survey Viewer one tool · installed once per operator facets on here progress slider overlay toggle imperial units facets on here layer styling contours and hillshade CRS and datum readout Entitlements resolve modules to tools: the union of every module the operator bought, plus any tool add-ons a tool is on if any entitled module includes it · a module contributes configuration, never a second copy PLATFORM RECORDS EVERY TOOL HANGS OFF Operator Company · parent node node Site, Project, Field, Asset Work Order Capture, Session, Visit Asset photo, ortho, video, splat Person roles on nodes, membership on nodes login optional
The viewer is one tool with facets. Each module that includes it switches on the facets and vocabulary its buyers need. Tools that only one industry uses simply live inside that module. Below the modules, the operator's entitlements become a flat set of tools, and every tool reads the same platform records.
ModuleTools it turns onWhat it contributes beyond the tools

Three rules that keep the overlap clean

  • A tool is installed once per operator. If two modules include the same tool, there is one copy. Each module contributes a configuration: which facets are on, default layers, terminology. Same code, different faces.
  • Entitlements resolve to tools. You sell modules; the platform computes the union of tools those modules grant. A tool can also be sold alone as an add-on, so a Construction operator can buy 3D Captures without the whole Survey and Mapping module.
  • Vocabulary is a module layer over platform names. The database always says Company and Work Order. The module says Project, Site, Field, or Asset for a company node, and Capture, Session, Visit, or Application for a work order. It is also how the rule that customer-facing copy never says "survey" becomes a setting instead of a code review.

Two consequences follow. A direct client company buying only Construction is an operator with one module and no Business module, which is exactly the Operator Model's direct case. And the existing code maps onto this without inventing anything: DroneVision is the Construction module and most of its tools, DSP is the Business module, and the imapshit tools are tools that several modules will share.

2Who the tenant is

The Operator Model from September 4 is adopted with its vocabulary and two changes. The first covers a company that comes in on its own and only wants DroneVision. The second replaces the model's flat companies with per-project user restrictions by a company tree, where membership at a node covers everything below it.

Platform · txDroneCo as vendor platform_admin · view-as is logged Pilot Institute partner · API key supports every operator provisions · bills wholesale OPERATORS · THE PAYING TENANT txDroneCo operator one · every module on Acme Drones a service provider · picks modules Summit Builders, direct operator record created silently A PI student student tier · hard limits Lone Star Aggregates parent company · billing and safety contact set here LSA Fort Worth inherits billing LSA South Dallas own billing Flight · Sep projects are child companies member at Lone Star sees both projects member at South Dallas sees one Ridge Builders parent · a general contractor Project 41 a job site Project 42 a job site Capture · wk 12 the same superintendent can hold a membership here and under txDroneCo Summit Builders parent · itself, hidden from its own users Project A what they see first Project B self-serve upload Capture upgrading to a full operator later is a flag, not a migration A first client parent · one level is enough Project a listing Shoot student tier caps nodes and work orders per month Every module, plan, and row-security policy keys on the operator. Portal membership sits on a node and covers its subtree. one record type for the tree, any depth · contacts, billing, deliverables, branding inherit down unless a node overrides them company node: parent or child, called Site, Project, Field, or Asset by the module work order: called Capture, Flight, Session, Shoot, or Application by the module
Four kinds of operator, each with a company tree of whatever depth its customers need. Lone Star Aggregates shows both cases from the mining example in one tree: its projects are child companies, LSA Fort Worth inherits the parent's billing and LSA South Dallas overrides it, and membership at the parent or at one project decides how much of the tree a person sees. The direct company gets an operator record it never sees. Pilot Institute plugs in above one kind of operator and pays for many of them at once.
LevelWhoRoles
PlatformtxDroneCo as the software vendorplatform_admin, acting through logged view-as
OperatorA service provider, a direct company, or a studentoperator_admin, operator_member, plus DSP's finer staff roles inside Work Orders
Company nodeThe operator's customer, at any depth of the tree: a parent company, a site, a project, a field, an assetcompany_primary, user, held at a node and covering its subtree

One tree of companies, one kind of work

Below the operator there are only two records that matter: a Company, which nests, and a Work Order, which hangs under a company node. A mining producer with six pits reporting to one manager is one parent with six children that inherit its billing and contacts. Another producer with ten pits that each have their own manager, billing, and deliverables list is one parent with ten children that override them. Same records, different tree.

IndustryParent nodeChild nodesWork orders
ConstructionGeneral contractorProjectsCaptures
Mining and aggregatesProducerSites, sometimes under regionsFlights or inventory visits
Survey and mappingEngineering firmProjectsSessions
InspectionUtilityAssets, nested as deep as the plant isInspections
AgricultureGrowerFieldsApplications
Creative and mediaClientProjectsShoots

The rules that make the tree work:

  • One record type, any depth. A node is a company, a region, a site, a project, or an asset depending only on where it sits and what the module calls that level. Every node can carry an address, a boundary, and coordinates; a "site" is simply a node with geometry.
  • Everything inherits down unless overridden. Billing contact, safety contact, deliverables list, branding, processing preferences, and portal access are set on a node and apply to its subtree. The six-pit producer sets them once; the ten-pit producer sets them per pit.
  • Portal membership is at a node and covers its subtree. The manager over six pits is a member at the parent and sees all six. A pit manager is a member at the pit and sees one. Nothing else is needed for per-project restriction.
  • Work orders attach to the node where the work happens and roll up. Appointments, flights, files, and deliverables hang under the work order. A recurring program creates work orders at a node on a schedule, and a campaign can span many nodes.
  • History stays with the node. A quarry flown for years under successive contracts is one node with a long list of work orders, so volumes and progress compare across all of them. When a property changes hands, the node moves to a new parent and keeps its history.

Neither app has this shape today. DroneVision has companies with flat projects, and DSP has clients with a site address on every work order. The tree replaces both, and a module that knows its customers usually have one level, as construction does, shows the parent and child as one screen.

Row isolation, done twice on purpose

The primary fence is in code: every database query goes through a data-access layer that requires the acting operator, and a test rule rejects any table access outside it. DroneVision already proved the team can hold that line with its guard helpers. The second fence is row-level security keyed on the operator, switched on with real policies, applied to server queries as well by setting the operator on the connection. The service role is confined to webhooks, the one-time data load, and the job worker. Membership lives in its own table so no policy ever has to look up the users table from inside a users policy, which is the recursion that made DroneVision turn RLS off.

3People

Operators have staff. Their clients have users, or just names on a list. Company nodes have site contacts and deliverable recipients, parents have billing and safety contacts, work orders have a crew. The way to keep this from sprawling is to separate four ideas that most systems mash into one "contact" record. Once they are apart, every case above is a relationship, not a new kind of record.

ObjectWhat it isOwned byHow many
IdentityA login: one per email address across the whole platformPlatformOne per human who signs in
MembershipAn identity's standing inside an operator or a client company, with a roleThe operator or companyOne per identity per organization
PersonA directory entry an operator keeps about a human: name, one email, one phone, optional company and title. Nothing else requiredThe operatorOne per human the operator deals with, login or not
Role assignmentA person filling a named role at a scope: billing contact on a parent company, site contact on a child node, safety contact on a work order, recipient on a deliverables listThe operatorAs many as needed; each is one row
ONE HUMAN, MANY ROLES ON THE TREE, ONE RECORD Site contact field on Project 42 crew types "Maria Ortiz, maria@…" match by email THE CLIENT'S COMPANY TREE Ridge Builders parent Project 41 child node Project 42 child node Capture · wk 12 THE OPERATOR'S DIRECTORY · PRIVATE TO THIS OPERATOR Person Maria Ortiz · email · phone at Ridge Builders, superintendent billing contact · Ridge Builders site contact · Project 42 recipient · Capture wk 12 role assignments · one row each · scoped to a node inherits to both projects Role catalog billing · safety · site contact · recipient tools add entries, never tables Deliverables list for Capture wk 12 site contact + billing contact + two bare emails · resolved at send time PLATFORM · OPTIONAL Identity one login for maria@… Membership at Ridge Builders · company_primary sees the whole subtree: Project 41, Project 42, every capture a membership at Project 42 alone would see only that project invite to the portal · links the Person · creates nothing twice Creating a Person is invisible: every field that asks for a human is a typeahead over the directory, and a new name and email becomes a Person on the spot. Role assignments decide who is notified, who is on a report, whom the crew calls. They never grant access. A billing contact set on the parent covers every node below it. Membership decides what she can see, and it follows the tree too: a node and everything under it. Another operator working with Maria holds their own Person record; only the Identity is shared, and only if she has a login.
The same human is one Person in the operator's directory, several role assignments scoped to nodes of the client's company tree, and optionally one platform identity with a membership at a node. Roles and membership both inherit down the tree, so a billing contact set once on the parent covers every project, and a login at the parent sees every project. Access and naming are decided by different objects, so a contact never has to become a user and a user never has to be re-entered as a contact.

Contacts without the contact-record burden

The Person record exists for everyone, but creating one is invisible. Every place that asks for a human is a typeahead over the operator's directory, and typing a name and email that is not there creates the Person on the spot. Deduplication is by email, so the same superintendent typed on three job sites is one Person with three assignments. What made contact records cumbersome was mandatory fields and separate screens; here the record is three fields and the screen is optional.

For the genuinely throwaway case, a deliverables list, a bare email address is allowed beside real people. A list is a mix of role keys, specific people, and addresses. Roles resolve at send time, so when the site contact changes the list follows without editing it. An address is promoted to a Person the moment it acquires a second attribute or a role.

Roles without role tables

Roles come from a catalog that tools contribute, the same way permissions do: billing contact, site contact, safety contact, superintendent, project manager, deliverable recipient, grower, applicator, emergency contact. Every assignment is one row: person, role, scope. The scope can be any company node, a work order, or an appointment inside it, and it inherits down the tree. Adding a new kind of contact for a new module is a catalog entry, not a migration.

What each object decides

  • Identity and membership decide what someone can see. Client users get membership at a company node, which covers its subtree, in place of the per-project restriction list the Operator Model describes. Operator staff get tool-level permission sets, which is where DSP's eight roles land as presets: pilot, dispatcher, data technician, finance.
  • Role assignments decide who is notified, who appears on a report, and whom the field crew calls. They never grant access.
  • The Person page shows every role a human holds across every company node and work order, which is what makes cleanup possible: merge two people and their assignments merge.

Two boundaries fixed now

  • A Person belongs to one operator's directory. When the same human works with two operators, each holds its own Person record; only the Identity is shared, and only if that human has a login. This keeps one operator's notes private from another and makes erasure tractable: anonymize the Person, keep the assignment history.
  • A Person can be affiliated with more than one client company, with a title at each, through a join table rather than a single company field.

All of this is kernel work for stage A: identities and memberships are already in the Operator Model, and Person plus role assignments are two tables and a catalog.

4Platform foundations

If the platform were built from a blank page with plugins, outbound APIs, and audits in mind, four things would go under the floor before the first feature. All four fit the transition; they only move a few items earlier.

ONE WRITE, ONE EVENT, MANY LISTENERS Web interface API key holder Plugin same contract for all three Typed API layer session or scoped key permission check · operator scope OpenAPI generated one transaction Postgres the row that changed outbox event both commit or neither does worker reads Worker fan-out job, retries Webhooks Plugins Audit log Notify Search Webhooks are signed and retried. Plugins run out of process in their own sandbox. The audit log is append-only and fed only from here, so it cannot be skipped. Replaying the outbox rebuilds search or re-delivers a webhook after a bug without touching the rows.
The mechanism the whole section turns on. Because the event is written in the same transaction as the change, nothing downstream can be missing an event, and everything downstream, including things that do not exist yet, subscribes the same way.

1. The typed API layer

One API defined as typed contracts, with the web interface as its first client and OpenAPI documentation generated from the same definitions. The public API for pushing data out is then the same API with a scoped key instead of a session, not a second system built later. For the DSP port this means handlers land in a framework-independent layer rather than bare framework routes, so porting DSP produces the public API as a by-product. The public surface is versioned by date with a fixed support window, the way Shopify and Salesforce learned to do it, so partners can build against it without fear. Built in stage C, used by every DSP tool from stage D on.

2. The event spine

Every state change writes a domain event to an outbox table inside the same transaction as the change. The worker fans events out to webhooks, plugin triggers, notifications, the audit log, and search. One table and one worker loop give you outbound webhooks, plugin reactivity, a complete audit trail, and replay after a bug. Built in stage A alongside the audit log, because the audit log is what auditors ask for first and it must never be skippable.

3. Manifests, extension points, and the plugin line

A tool ships a manifest declaring its schema namespace, API routes, screens, permissions, events emitted and consumed, jobs, and settings. A module manifest selects tools, sets their facets, and supplies vocabulary and templates. Modules are first-party, trusted, in-process. Plugins are third-party or customer-built, untrusted, out of process.

  • Plugin tier one, from stage B: API keys with scopes, OAuth apps for third parties, signed webhooks with retries. This covers most integrations and needs no runtime.
  • Plugin tier two, later: a sandboxed runtime for plugin code that reacts to events or adds panels, likely Cloudflare Workers for Platforms so each plugin runs isolated under our dispatch namespace, with iframe extension slots that talk to the page over a small versioned message API.
  • Extension points from day one: navigation slots, detail-page panels, event hooks, export formats, map layer sources. First-party tools use them first, so the mechanism is proven before anyone outside touches it.

4. Compliance designed in

SOC 2 and ISO 27001 are mostly organizational controls, and about a third of the evidence is technical. The design makes the technical evidence generate itself.

Control an auditor asks forWhat produces the evidenceStage
Complete audit trailAppend-only audit table fed only from the outbox, with actor, operator, source, and before-and-after state; exportable per operatorA
Access controlMFA enforced for platform staff, logged view-as, SAML single sign-on available to larger operators, API keys stored hashed with scopes and expiry, no service-role credential in the request pathA to B
Tenant isolationThe end-to-end suite proving one operator cannot read another, plus row-level security as the second fenceA
Change managementThe gate: lint, types, unit coverage, end-to-end suites, protected main, deploy logsExists
Backup and recoveryPoint-in-time recovery on Postgres, versioning on R2, a documented restore test twice a yearA
Secrets and encryptionPlatform secrets in the hosting vaults, operator-supplied secrets encrypted at the application layer, rotation dates recordedB
Logging and monitoringCentralized logs and error tracking with retention, alerting, uptime checksA
Vendor managementSubprocessor list: Vercel, Supabase, Cloudflare, Modal, Resend, Stripe, each with a published SOC 2 report; Railway and DigitalOcean drop off it once the last droplet service movesA
GDPR and CCPAData processing addendum, per-operator export and erasure endpoints, retention settings per data class, a region field on the operator so EU residency is routing later rather than a rewriteB
Evidence collectionA compliance automation platform such as Vanta or Drata connected to GitHub, the hosting accounts, and Google WorkspaceA

Order: SOC 2 Type I, then Type II after a monitoring window, then ISO 27001 only if a buyer requires it. The attorney items from the Operator Model, the operator agreement, the partner agreement, and the data processing addendum, belong to the same workstream and start in stage A.

5. Kernel decisions the modules force

Working through the module set surfaced five things that belong under the floor rather than inside any tool.

  • Company is a tree. Inspection needs asset hierarchies, Construction needs projects under a contractor, Aggregates need pits under a producer, and one producer puts one manager over six pits while another gives every pit its own billing. One record type that nests, with per-node overrides inherited from the parent, covers all of it without a second record type.
  • Work orders and recurring programs are platform records. Business, Inspection, and Aggregates all need "a schedule that creates work orders at a company node." One kernel mechanism, three views of it. The recurring engine is rebuilt as a kernel service in stage D rather than ported as work order code.
  • Offline sync is a kernel library. Application records, field notes, and inspection findings are entered where there is no signal. The local-store-and-upsert pattern from txFieldNotes becomes something any field-facing tool declares in its manifest.
  • Metering covers storage and delivery, not only processing. A media-only plan has almost no processing cost; its cost is stored bytes and delivered video. The usage table carries those metrics from stage B.
  • Assets get a content hash at ingest and a retention hold. Chain of custody and any dispute over a photo need proof a file has not changed and a way to block deletion during a hold. Hashing at upload is nearly free now and impossible to retrofit.

What this changes in the stages

  • The outbox and the audit log become stage A prerequisites, beside the migration runner and row-level security.
  • The typed API layer is built in stage C before the first DSP tool, so the port produces the public API.
  • Manifests, the permission catalog, and the extension points are built in stage C rather than after.
  • The company tree, work order programs, the asset hash, and the metering shape are in the stage A schema; offline sync is designed in stage C and shipped with the first field tool.

Everything else on this page stands unchanged. Starting over and transitioning arrive at the same platform, which is the argument for the transition over a clean rewrite.

5Heavy services

A heavy service is anything that takes longer than a web request or needs hardware the web tier does not have: photogrammetry, tiling, splat training, transcoding, packaging, thermal detection, point cloud work. We will not know the full list up front, and the architecture should not need to. It needs one contract every service speaks, three compute shapes a service can declare, and a routing rule that lets tools ask for a capability rather than a specific engine.

Designed fresh, with no loyalty to where things run today, the heavy side collapses onto one serverless compute plane. Nothing below is a server anyone patches, and every provider bills by use.

THE JOB CONTRACT · WHAT EVERY SERVICE SPEAKS A tool asks for a capability orthomosaic jobs row capability · operator · inputs in R2 params · priority · cost class Dispatcher routes by capability · spawns by API honors bring-your-own engine Warm containers · minutes CPU · kept warm by day · scale to zero at night GPU on demand minutes to an hour · CUDA · billed per second Big container · hours large memory, terabytes of scratch, a day at most Jobs API · claim, heartbeat, progress, complete, fail the only way a service touches the platform · worker token · no database credentials R2 · inputs and outputs under the operator prefix services pull inputs by presigned URL and push outputs plus a manifest.json · nothing transits the web tier status, progress, log, cost written back Adding a service means: a container that speaks the jobs API, a manifest naming the capabilities it fulfills and its compute class, and nothing else. A tool never names an engine. Swapping ODM for another photogrammetry engine, or letting an operator bring their own, is a dispatcher rule.
The contract is small on purpose. A job names a capability, not a service; a service claims work through one HTTP API and moves files only through R2. Because of that, a service we have not thought of yet, in any language on any provider, plugs in the same way the ones we have do.

Planning for services we do not know yet

  • Capabilities, not engines. Tools request orthomosaic, tiles, splat, transcode, package, detect_thermal, volumes. Services advertise which capabilities they fulfill. A new engine is a new fulfiller, and an operator's bring-your-own key is a routing preference.
  • Three compute shapes, declared per service. Warm containers for minutes of CPU, GPU on demand for training and inference, a big container for hours. A service says which it needs; the dispatcher already knows how to run each shape, and all three run on the same plane.
  • One contract, spoken over HTTP. Claim with a lease, heartbeat, report progress, complete with a manifest of outputs, or fail with a reason. Inputs and outputs are presigned R2 URLs in the job payload. No service holds database credentials, which is what lets a third-party or a wrapped commercial API participate safely and what keeps the audit boundary clean.
  • Adapters for engines we rent. A commercial API such as a hosted photogrammetry or a mapping platform is wrapped as a service that fulfills a capability by calling out, so it looks like every other service to the platform and can be replaced without touching a tool.
  • Cost is a field on the job. Every job records its class and seconds, so metering per operator and the margin on each plan fall out of the same table without a separate accounting system.

Back-office jobs use the same plane

DSP runs five cron jobs, a dynamic report scheduler held in memory, the recurring work order spawner, and an outreach queue, all inside the web process. None of that has a home in a serverless web tier, and none of it belongs there.

Vercel Cron one tick per minute enqueue due jobs table · Postgres fixed crons as rows report schedules with next_run_at per-operator daily caps as counters recurring spawn per operator timezone claim · lease · heartbeat Warm containers · Modal spawned per job by the dispatcher kept warm by day, zero at night talk only to the jobs API lease and heartbeat from the packager Outside services FAA grids · TFRs · NWS · OSRM · Resend Our compute ODM cluster · txTiler · packager GPU jobs splat pipeline on Modal The web tier only answers requests in seconds. Everything longer is a job on the same plane the heavy services use, in whichever shape it needs.
One queue, one contract, one compute plane. Vercel Cron does exactly one thing: it wakes the enqueuer. Background jobs from the back office are just small heavy services, so they run where the tiler and the splat engine run, with the lease and heartbeat pattern DroneVision's packager already proved.
WHERE EACH SERVICE LIVES · DESIGNED FRESH CLOUDFLARE · FILES, EDGE, DNS, PLUGIN SANDBOXES VERCEL AND SUPABASE · WEB, API, DATABASE, SECONDS-LONG WORK MODAL · EVERY HEAVY SERVICE · BILLED PER SECOND · NO SERVERS TO PATCH DNS · edge cache R2 · media storage originals · derived · deliverables · no egress fees Tile and video delivery range requests from R2 · signed URLs Free tools · plugin sandboxes Workers static · Workers for Platforms later Web app and typed API Next.js · API runtime-agnostic Supabase Postgres · PostGIS · Auth Dispatcher jobs API · spawns on Modal Image pipeline thumbnails · EXIF · seconds Cron tick · log parser enqueues due jobs · Rust binary WARM CONTAINERS · MINUTES · CPU Tiler Transcoder Deliverables engine Layout renderer Point clouds Volumes Fetches · email · hooks GPU ON DEMAND · MINUTES TO AN HOUR Splat engine · L4, L40S Thermal detection · T4 room for services we have not named yet, any shape BIG CONTAINERS · HOURS · LARGE MEMORY · UP TO A DAY Photogrammetry · ODM Large point cloud jobs fallback only: per-job spot machine on a hyperscaler, same contract, if a job exceeds Modal's memory ceiling Flows: browser uploads go straight to R2 by presigned URL. Every service pulls inputs from R2 and pushes outputs to R2. The edge serves tiles, images, and video from R2 by range request. Only job metadata crosses provider boundaries. The dispatcher spawns a Modal function per job; the function reports back through the jobs API and dies. Four providers, none of them a server we own. Railway and DigitalOcean leave the picture once the last droplet service is a container.
Three lanes hold three kinds of thing: files and edge, the application and its database, and every heavy service. The heavy lane is one provider because it can run all three compute shapes, bills by the second, and leaves no machine idle or unpatched. The dashed box is the only hedge, and it uses the same contract.

What each service needs to run

ServiceCapabilityWhat it needsClassLivesToday
Media storagestore, deliverS3-compatible object store, presigned multipart uploads, lifecycle rules, versioning, zero egress feesStorageCloudflare R2In use by DroneVision and txSplat
Delivery edgeserve tiles, video, imagesRange requests, cache, signed URLs, custom domains per operatorEdgeCloudflare in front of R2Partly; txTiler serves its own tiles
Photogrammetryorthomosaic, surface model, point cloudDocker, tens of gigabytes of RAM, many cores, hundreds of gigabytes of scratch disk, hours per jobBig containerModal, spot machine fallback for the largest flightsDigitalOcean via ClusterODM autoscale; WebODM Lightning as fallback
TilertilesGDAL, Python, CPU, moderate RAM, disk for the source rasterWarm containerModaltxTiler on a DigitalOcean droplet
Splat enginesplatNVIDIA GPU with CUDA, COLMAP, gsplat, ffmpeg, twenty to sixty minutes per sceneGPU on demandModalPipeline proven on Modal
Transcodertranscodeffmpeg, CPU or a small GPU, minutes per video, diskWarm containerModalInside the photo packager
Deliverables enginepackageNode, image processing, zip streaming, PDF generation, diskWarm containerModalphoto-packager on the txTiler droplet
Image pipelinethumbnail, exifsharp, exifr, seconds per image, runs in batchesServerlessVercel FunctionsIn DroneVision
Layout rendererprintHeadless Chromium or a native map renderer, fonts, minutes per sheetWarm containerModalDoes not exist
Point cloud processingreproject, align, copcPDAL, PROJ with geoid grids, CPU, RAM scaled to cloud sizeWarm container, big container for very large cloudsModalPointCloudZ desktop; a cloud worker exists
VolumesvolumesGDAL and numpy over surface models, CPU, seconds to minutesWarm containerModalDoes not exist
Thermal detectiondetect_thermalPython, YOLO weights, GPU for throughput or CPU for small batches, radiometric extractionGPU on demand, smallModalFlask service on the txTiler droplet
Flight log parserparse_logRust binary, seconds per logServerlessVercel FunctionsIn the flight-logbook crate
External fetchesairspace, weather, routing, geocodeHTTP with rate limits and caching, retriesWarm containerModalInside the DSP web process
Email and webhooksnotify, webhookResend, signed HTTP delivery with retriesWarm containerModalEmail in three apps; webhooks do not exist

The "Today" column is the migration list. Every service on a droplet becomes a container on Modal, in whichever of the three shapes it needs, which retires the txTiler droplet, the ODM cluster, and Railway. One thing to verify before the photogrammetry move: Modal publishes a full-day timeout and three terabytes of scratch disk, but not its memory ceiling, and the largest flights need tens of gigabytes. Ask for the number; if it is short, the dashed fallback in the map handles only those flights through the same contract.

Two more decisions taken fresh rather than inherited. The API is written on a runtime-agnostic framework, so it runs inside the Vercel deployment today and could move to Cloudflare Workers beside the plugin sandboxes without a rewrite. Events and jobs stay in Postgres rather than a queue product, because auditability and replay matter more here than throughput, and one database is one less system to certify.

Portability: switching providers later

The freedom to move to EC2, S3, or anywhere else comes from three interfaces, not from good intentions. Services and tools touch providers only through the S3 API for files, the jobs API for work, and an auth adapter for identity. A lint rule flags a provider SDK imported anywhere else.

PieceWhat a switch costsWhat changes
StorageConfig change and a bucket copy; R2 already speaks S3Economics: S3 charges egress, and delivery is egress-heavy, so model CloudFront first
Heavy computeOne dispatcher adapter that starts the same containers on EC2, Batch, or a GPU cloudNothing inside any service; the fallback adapter exists early on purpose
DatabaseDump and restore to RDS or AuroraNothing; PostGIS and row-level security travel with it
Web and APIThe API runs on any runtime; the web app runs on AWS through OpenNext or a containerDeployment only
EdgeCloudFront in front of S3 instead of Cloudflare in front of R2; DNS stays putCache rules
AuthenticationRewrite the auth adapter for Cognito or WorkOSThe one thin seam: the token hook, invites, single sign-on setup. Permission checks read a standard JWT and do not change
Plugin sandboxesAnother isolation runtime such as Lambda or FirecrackerThe least portable piece, by design; plugins arrive late and speak a small versioned API
SchedulingAny scheduler that can call one URLNothing

Four rules keep this true. The dispatcher has a provider interface from day one with two implementations. Infrastructure is declared as code, so a second environment is a re-apply, not a rediscovery. Once a year one real job runs end to end on the fallback provider, because a portability claim never exercised is a hope. And the reasons to switch are named in advance: an enterprise buyer that requires AWS, EU data residency, GPU availability, or a scale where owning capacity beats renting it by the second.

Part 2

The roadmap

Where we start, what we decided, and the order of events.

6Where we start

The picture that matters is not which app is bigger. It is that the two apps share no identity, no client record, and no project record, so nothing built on either can be sold to a second operator. The fix is one record above both, and every module keyed to it.

TODAY TARGET txDroneCo staff Google Workspace login Client companies Supabase Auth login DSP Express · one 13.7k-line file SQLite on Railway 308 routes · 74 tables work orders · CRM · invoices airspace · equipment · tasks no operator · flat clients DroneVision Next.js 16 on Vercel Supabase Postgres · R2 31 tables · 48 migrations galleries · maps · 360 · solar share links · self-serve upload no operator · flat projects × no shared client, project, or user ids Railway volume uploads on local disk ODM cluster · txTiler · R2 packager queue with leases THE PLATFORM · ONE RECORD ABOVE EVERYTHING Operator Company · parent node node sites, projects, fields, assets Work orders captures, sessions, visits memberships · people plans · entitlements · billing audit log · event spine one login per person · one company tree per client · one work order record for every module operator_id on every row FROM DSP Work Orders CRM Invoicing Airspace FROM DRONEVISION DroneVision Mapping Ground Capture Solar Inspection FROM THE FIELD TOOLS · AND NEW Field Tools Quoting 3D Captures Survey Viewer every module reads and writes the same kitchen Shared kitchen · one Postgres · one R2 · one job contract ODM cluster · txTiler · splat pipeline · solar detector · email · FAA and weather fetches
Swipe sideways to see the target. Left: two production apps with two logins, two databases, and a red mark where the shared identifiers should be. Right: the blue band is the new platform, drawn as the record chain every tool shares, an operator, a company tree, and work orders, with memberships, plans, and billing beside it. The tool boxes are the same code you already run, regrouped, and the kitchen is shared by all of them.

Where each app stands

These numbers come from reading the code, not from memory. They explain why the plan treats the two apps so differently.

What a sellable platform needsDSP todayDroneVision today
An operator tierNone. Single operator, zero tenant columns across 74 tables.None. Tenancy stops at client companies; the admin role sees every company.
Identity anyone can joinGoogle Workspace only, locked to the txdroneco.com domain, home-grown session tokens.Supabase Auth with invites, recovery, and a mobile bearer token.
A database that scales past one machineSQLite on a Railway volume, 1,036 inline SQL calls, 57 synchronous transactions.Supabase Postgres with PostGIS, 48 migration files.
Ordered migrations153 try-and-swallow ALTER statements at boot.Migration files exist but are applied by hand.
Row isolation as a second fenceHand-rolled per-route checks and field redaction.RLS enabled with no policies; every query uses the service role.
Object storageLocal disk uploads, 50 MB cap.R2 with presigned direct uploads and multipart.
Background jobsFive in-process cron jobs plus a dynamic report scheduler held in memory.Claim-based queue with leases and heartbeat for the packager; ODM runner still in-process.
A test gateNone.Unit coverage thresholds, 13 end-to-end suites, git hooks that block a push.
Domain depth308 routes, 74 tables, a written field-permission model, recurring programs, invoicing, CRM sequences.31 tables, focused on delivery and viewing.
Front end that ports cleanlyYes. 33 pages call one file of 304 API functions and touch no database.Already on the target stack.

Read the last two rows together. DSP has the product and a front end that survives the move. What does not survive is its server, and that is the part that has to change no matter what.

7The DSP platform decision

You asked whether DSP is on the right platform. The server is not, and the honest part is that it would be rewritten under any option below, because adding an operator column and leaving single-writer SQLite touches every one of its inline SQL calls and every synchronous transaction. Once the rewrite is a given, the only question is what to rewrite into.

Recommended

Rewrite into the platform's own stack

A typed API layer inside the DroneService Pro codebase, one handler per DSP API function, against Postgres through Drizzle, with OpenAPI generated from the same definitions so the public API is a by-product. The 304 functions in api.js become the contract, and a response diff against old DSP is the acceptance test for each group. One login, one database, one design system, one job queue, one gate.

Not recommended

Keep Express, swap SQLite for Postgres

The same SQL rewrite, but the monolith, the second runtime, the second deploy, and the second auth layer all survive. Worth it only as a stepping stone if a multi-operator demo were needed in about six weeks. You said there is no such deadline.

Not recommended

A separate API service

Hono or NestJS on Railway with the platform as the only front end. Same rewrite as the recommendation plus a duplicated auth and entitlement layer to keep in sync. The fallback if some DSP route ever outgrows a Vercel function, which the job worker should prevent.

Not recommended

Keep SQLite on Cloudflare

D1 or a SQLite Durable Object per operator preserves the SQL dialect, but the driver is different anyway, PostGIS is lost for maps and 360 capture, and Supabase Auth is cut off. The dialect was never the hard part; it is about 200 date literals.

What makes the rewrite safe is the seam that already exists. The front end never touches the database; it calls one file. That file is the specification.

THE SEAM 33 pages unchanged React screens api.js 304 functions, names kept the contract old base URL Express index.js 1,036 inline SQL calls SQLite 74 tables, one operator new base URL Typed API layer grouped by tool, OpenAPI out Drizzle Postgres operator_id everywhere response diff same request, same JSON, or the port is not done Port one URL group at a time. The diff runs against a snapshot of the old database, so a passing group is proven, not assumed.
The front end keeps calling the same function names; only the base URL moves. Because both servers answer the same requests during the port, every group of routes can be checked mechanically before it replaces the old one.

The specific technical calls inside that recommendation

  • Drizzle, not the Supabase client, for DSP domains. Invoice numbering, recurring work order spawning, and the outreach daily cap need real transactions, and a typed schema is the single biggest accuracy lever when agents do the porting.
  • Migration files generated from DSP's own table definitions, plus the operator column, with fixed conversion rules for the SQLite habits: date literals, integer booleans, insert-or-ignore, and the two date functions.
  • Files go to R2 through the presign and multipart code DroneVision already has.
  • Supabase Auth with the Google provider and a per-operator list of allowed email domains replaces the hard-coded txdroneco.com lock.
  • Pages port as client components first. Router links change, inline styles stay, the 3,400-line work order detail page moves as one piece and is split afterwards.

What would reopen this decision: a hard commitment to show a multi-operator product within about six weeks. Nothing else in the facts points elsewhere.

8The order of events

The kernel is not built from scratch in an empty repository. It is grown inside DroneVision, because DroneVision is production, multi-company, and closest to the target. Once txDroneCo runs on the operator record, DroneVision is sellable on its own, and the DSP tools land on the same record afterwards. The codebase is restructured into the DroneService Pro monorepo when the first DSP tool arrives, not before.

STAGES · SIZE · WHAT BECOMES SELLABLE The company tree and work order record are built in A, carry DroneVision's projects in B, and receive DSP's clients and work orders in D. Every later stage hangs tools on them. A LARGE B MEDIUM C MEDIUM D LARGE E SMALL F LARGE G MEDIUM H LARGE Operator + tree inside DroneVision · RLS on Sell the portal signup · Stripe · view-as Monorepo kernel · job contract DSP daily core clients into tree · WOs Cutover one weekend Rest of DSP tasks · CRM · airspace · reports Partner layer Pilot Institute New modules field · 3D · viewer · logbook Sellable DroneVision on its own txDroneCo moves old DSP goes read-only Sellable full suite, wholesale
Size labels are relative to each other, not calendar estimates. The small circle on each stage shows its status, set on the stage cards below. The first sellable moment comes early because the operator layer alone turns the existing portal into a product; the biggest single stage is the DSP daily core, and it runs while txDroneCo keeps using old DSP.
  1. A

    Operator layer inside DroneVision

    Large
    Built
    Operators and memberships tables, the company tree with work orders under its nodes replacing DroneVision's flat companies and projects, the role split, the staff-side scoping audit across 50 files and 18 admin routes, operator branding and identity settings, plans and limits, a migration runner so schema changes stop being a hand step, and row-level security back on. This is the Operator Model's Phase 1 plus the two prerequisites it names.
    Staff see
    The same DroneVision admin, now with an operator switcher that shows one entry.
    Customers see
    Nothing changes.
    Keeps running
    DSP untouched. All of DroneVision's processing.
    Exit
    txDroneCo is operator one. An end-to-end suite proves one operator cannot read another's data.
  2. B

    DroneVision sellable on its own

    Medium
    Built
    Self-signup for operators and direct companies, Stripe subscriptions with metered processing, per-operator subdomains, bring-your-own processing key, logged view-as, an operator-visible audit log. The Operator Model's Phase 2. Plus two lessons from platforms that opened APIs before us: the public API is versioned by date from its first outside caller, with a published deprecation window, and every operator and partner gets a sandbox operator seeded with example companies and work orders, isolated by the same row security.
    Staff see
    View-as replaces unrestricted admin. New operators appear in the switcher.
    Customers see
    Their operator's brand, not ours, in email and on the portal.
    Keeps running
    DSP untouched.
    Exit
    Two or three friendly providers on paid plans, a direct company on the portal-only plan, and one outside integration built against a sandbox without help from us.
  3. C

    Monorepo and kernel extraction

    Medium
    Built
    The repository becomes DroneService Pro. Identity, operators, entitlements, storage, share links, and the design tokens move into kernel packages. DroneVision's code becomes the DroneVision, Mapping, Ground Capture, and Solar Inspection tools with manifests, and the Construction module is the first bundle defined over them. The jobs table, the jobs API, and the dispatcher are stood up, and the ODM runner and the packager become the first services on the contract.
    Staff see
    A platform shell with module and tool navigation. Only the DroneVision tools are lit.
    Customers see
    Nothing changes.
    Keeps running
    DSP untouched.
    Exit
    A module can be turned off for an operator and its tools' routes return a clean refusal. The gate runs per tool.
  4. D

    DSP daily core ported to parity

    Large
    Built
    In dependency order: clients and contacts loaded into the company tree as parents and child nodes, equipment, users and the field-permission model, then Work Orders with schedule, recurring programs, and time entries, then Invoicing and the service catalog. Each group passes the response diff before the next starts. The one-time SQLite-to-Postgres load is written now and rehearsed against snapshots. Bulk import and export ship as tools beside the client and equipment port, using the same validation as the API, because every operator arrives with a spreadsheet and every operator must be able to leave with their data.
    Staff see
    A parallel environment they can try with a copy of real data. Daily work stays in old DSP.
    Customers see
    Nothing changes.
    Keeps running
    Old DSP, fully writable, until the next stage.
    Exit
    The field team completes a normal week in the new environment on copied data without reaching for old DSP, and a sample operator's spreadsheet of clients, their sites or projects, and equipment loads end to end through the import tool.
  5. E

    Cutover weekend

    Small
    Built
    Nothing new. The runbook below is executed.
    Staff see
    Friday evening old DSP goes read-only; Monday morning they log into DroneService Pro.
    Customers see
    Nothing changes.
    Keeps running
    Old DSP, read-only, at a legacy address for six weeks.
    Exit
    Row counts and checksums match, the diff is clean, and the first Monday closes without a rollback.
  6. F

    The rest of DSP

    Large
    Built
    Tasks and projects, then CRM with sequences and the outreach queue on the job worker, then Airspace with the FAA and weather fetches moved to the worker, then reports and dynamic schedules, then Quoting absorbed from txQuoter.
    Staff see
    Tools light up in the shell as each lands. Until then the read-only old DSP answers for those areas.
    Customers see
    Nothing changes.
    Keeps running
    Everything cut over in the previous stage.
    Exit
    Old DSP is switched off. The Business module is sellable in full.
  7. G

    Partner layer

    Medium
    Built
    Partner records with API keys, provisioning keyed by the partner's own user ids, one-time login handoff using the invite mechanism, partner billing by active operator count, partner co-branding, and the student tier with hard limits. The Operator Model's Phase 3.
    Staff see
    A partners page and a student cohort in the switcher.
    Customers see
    Students see Pilot Institute's mark beside their own brand.
    Keeps running
    Everything.
    Exit
    A pilot cohort provisioned by Pilot Institute's system, invoiced once, with the graduation terms settled in the contract.
  8. H

    New and merged tools, new modules

    Large
    Built
    Field Tools from txFieldNotes with the QC, GCP, and GSD tools embedded; 3D Captures from the txSplat pipeline and PointCloudZ; Survey Viewer as the first wholly new tool; Pilot Logbook from the flight log parser. With those in place the Survey and Mapping module is defined, and Inspection and Agriculture follow as bundles plus their few industry-only tools.
    Staff see
    New tools in the shell, each reachable through the modules that include it.
    Customers see
    Styled map layers and print layouts in the portal, 3D scenes beside galleries.
    Keeps running
    Everything.
    Exit
    The Survey and Mapping module is sellable. The free tools on imapshit.com run the same code the platform embeds.

9DSP port playbook

Contract first, one URL group at a time. The client file that lists the 304 API functions is the specification; each function gets a route handler with the same path, the same request shape, and the same response, checked by the diff. The order below follows the foreign keys.

OrderGroupRoutesWhy here
1Clients and contacts27Everything else keys into clients. DSP's clients become parent nodes of the company tree and DroneVision's companies and projects are already there from stage A.
2Equipment, users, roles~30Small, referenced by work orders. The P0 to P5 field permissions are encoded in the data-access layer before any work order code exists.
3Work orders, schedule, recurring63The daily core. Recurring spawning moves to the job worker in the same step.
4Invoices and services20Depends on work orders and clients. Invoice numbering needs a real transaction.
·CutovertxDroneCo moves here.
5Tasks and projects43Large but not daily-critical.
6CRM and outreach90The biggest group. The send queue with its daily cap rides the job worker.
7Airspace, field notes, reports, remaining admin~35External fetches move to the worker last; report schedules become rows.

Conversion rules that apply to every group

SQLite habitCountBecomes
datetime('now')197now() on timestamptz columns, set by the schema default, not the query
Integer booleans45 columnsboolean with the load script mapping 0 and 1
INSERT OR IGNORE27ON CONFLICT DO NOTHING with the conflict key named explicitly
strftime, julianday16date_trunc, to_char, interval arithmetic
Synchronous transactions57Drizzle transactions; anything that awaited outside the block moves inside or before it
Try-and-swallow ALTERs153Ordered migration files, generated once from the current schema and never edited by hand at runtime
Application UUID stringsall keysKept as-is. Invoice numbers, share links, and external references survive unchanged

Cutover day

  1. Freeze. Friday evening, flip old DSP to read-only with one environment flag that blocks every non-read request. Tell the field team.
  2. Load. Run the rehearsed script, already run against snapshots at least three times: SQLite to Postgres, every row stamped with txDroneCo's operator, booleans and timestamps converted, uploads copied from the Railway volume to R2 under the same keys.
  3. Verify. Row counts and checksums per table, then the response diff against the last answers old DSP gave.
  4. Go live. Point the DSP address at the platform. Old DSP stays reachable read-only at a legacy address for six weeks.
  5. Rollback window. Until Monday morning: flip old DSP writable and drop the operator's Postgres rows. Because writes were frozen, nothing needs reconciling.

Why not module by module with both databases live

DSP's domains are cross-joined: a work order references clients, equipment, invoices, tasks, and users, and 57 transactions span those boundaries. Dual writes across SQLite and Postgres for a small team is the riskiest path on the list. One rehearsed load and one frozen weekend is safer than months of two sources of truth.

10Risks and how each is held

RiskHeld by
Daily operations disruptedOld DSP keeps running through stages A to D. Cutover is one frozen weekend with a rollback window. Old DSP stays read-only for six weeks after.
Permission regressionsThe written P0 to P5 field model is encoded in the data-access layer before any work order code, and the end-to-end suite that proves operator isolation is written in stage A and extended per module.
Data loss in the loadThe load script is rehearsed against snapshots at least three times, verified by counts and checksums, and the UUID keys make every reference traceable.
Two stacks living too longThe cutover is an exit criterion for stage E, not a someday. Stage F ends with old DSP switched off.
Scope creep from the new viewerSurvey Viewer is deliberately last. Nothing in stages A to G depends on it.
Modules multiply faster than toolsA module is mostly data: a manifest that selects tools and supplies vocabulary and templates. Adding an industry should cost a manifest and at most a few new tools, never a fork.
The names and brands shiftEvery module and tool has a permanent key and a display name. Renaming is a settings change, not a refactor. This page works the same way.
Subdomains per operatorVercel wildcard domains need Vercel to run DNS, which conflicts with keeping DNS at Cloudflare. Options: a Cloudflare Worker in front that maps host to operator, per-operator records added by API at signup, or path-based routing at first.

11Decisions still open Needs Jared

Made already: the platform is named DroneService Pro, DroneVision is a module inside it that can also be bought alone, operators are the paying tenant, direct companies get a hidden operator record, DroneVision's stack is the platform, DSP is rewritten into it rather than modernized in place, there is no near-term demo deadline. What remains shapes stages B and G.

  1. Pricing basis

    Per seat, per processed map, per gigabyte, or a mix, and whether tools can be bought alone or only through modules.

    Recommendation A base subscription per operator, modules on top, a short list of tool add-ons, metered processing underneath. Processing is the only real cost of goods.

  2. Staff access

    Keep unrestricted admin for our staff, or move to a logged view-as mode.

    Recommendation Logged view-as, as the Operator Model argues. Providers will be putting competitors' client lists on our platform.

  3. Pilot Institute deal shape

    Hosted access with provisioning, or a source or self-host license, and who owns the student after graduation.

    Recommendation Hosted with provisioning. Settle graduation in the contract, negotiated in parallel with stage A.

  4. Legal paperwork

    An operator agreement, a partner agreement, and a data processing addendum before stage B goes live to anyone outside txDroneCo.

    Recommendation Start with the attorney during stage A. The contract will take as long as the code.

Appendix

Working registry

Names, modules, tools, and assignments. Edits here flow through every diagram and sentence above.

Modules and tools Editable

Modules are what you sell; tools are what you build. Every one has a permanent key the code uses and a display name that is yours to change. Rename, add, remove, and decide which tools each module turns on. The diagrams, the sentences, and the module table on this page follow. Keep names short so they fit the drawings.

local only

Platform and vocabulary

Modules what you sell

Tools what you build

Which tools each module turns on

Changes save as you make them.