src/models/ (see Database Schema Overview), the Isaac Sim EC2-instance accounting objects — 4 tables, 3 views, 2 PL/pgSQL functions and 3 triggers — exist only as hand-written SQL inside a single Alembic migration and are read/written with a raw psycopg2 cursor, never through the ORM.
This page documents those objects, why they diverge from the ORM, the (currently dormant) code that uses them, and the exact set of database objects a DB-layer refactor must recreate.
Everything lives in one migration
All Isaac Sim database objects are created by exactly one migration and are never altered by a later one:db.Model for any of these objects:
db.create_all()will never create them. The dev-mode shortcut (APP_ENVIRONMENT=dev+python main.py) only reflects ORM models, so a dev database bootstrapped that way is missing the entire Isaac Sim schema — tables, views, functions and triggers. See Migrations, DB Connection & Dev Mode. Onlyflask db upgradeprovisions them.- Alembic autogenerate cannot see them.
flask db migratebuilds diffs fromdb.metadata, which knows nothing about these tables. Any future change to the Isaac Sim schema must be hand-authored raw SQL (op.execute(...)/op.create_table(...)), exactly likee85b13d42b08— you cannot autogenerate it. - Views, functions and triggers have no ORM equivalent at all. SQLAlchemy models cannot express
CREATE VIEW,CREATE FUNCTIONorCREATE TRIGGER, so even if models were added for the tables, the view/function/trigger layer would still be raw SQL.
src/models/__init__.py is empty; ORM models register on db.metadata only as a side effect of a service importing them. The Isaac Sim tables never had a model to import, so they were provisioned as raw DDL from day one rather than being an accidental omission.Entity model
The 4 tables
isaac_sim_instances
One row per launched Isaac Sim EC2 instance. user_id FK is ON DELETE CASCADE; drone_id FK is ON DELETE SET NULL.
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | |
user_id | INTEGER NOT NULL | FK user.id CASCADE, idx idx_isaac_instances_user |
drone_id | INTEGER NULL | FK drone.id SET NULL, idx idx_isaac_instances_drone |
instance_id | VARCHAR(50) NOT NULL UNIQUE | EC2 instance id |
instance_type | VARCHAR(20) | default g6e.4xlarge |
public_ip / private_ip | VARCHAR(50) NULL | |
availability_zone | VARCHAR(50) NULL | |
is_spot | BOOLEAN | default true (spot vs on-demand) |
state | VARCHAR(20) NOT NULL | default launching (also running, stopped, …), idx |
launch_time / stop_time | DATETIME NULL | |
auto_shutdown_time | DATETIME NULL | 2 h after launch; idx idx_isaac_instances_shutdown_time |
session_id | VARCHAR(100) NULL | idx |
total_runtime_hours | NUMERIC(10,2) | default 0 |
estimated_cost | NUMERIC(10,2) | default 0 |
ebs_snapshot_id | VARCHAR(50) NULL | snapshot taken on auto-shutdown |
created_at / updated_at | DATETIME | default CURRENT_TIMESTAMP |
created_at DESC (idx_isaac_instances_created). updated_at is bumped by a trigger (below).
isaac_sim_usage_tracking
Immutable session-history log; one row appended per completed session. Both FKs are ON DELETE CASCADE.
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | |
user_id | INTEGER NOT NULL | FK user.id CASCADE, idx |
instance_id | INTEGER NOT NULL | FK isaac_sim_instances.id CASCADE, idx |
session_start | DATETIME NOT NULL | idx session_start DESC |
session_end | DATETIME NULL | |
duration_minutes | INTEGER NULL | |
cost | NUMERIC(10,2) NULL | |
created_at | DATETIME | default CURRENT_TIMESTAMP |
isaac_sim_budget
One row per calendar month (month = YYYY-MM, UNIQUE). Rolls up spend across all users for cost-cap enforcement.
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | |
month | VARCHAR(7) NOT NULL UNIQUE | e.g. 2026-07; idx month DESC |
total_spent | NUMERIC(10,2) | default 0 |
total_hours | NUMERIC(10,2) | default 0 |
session_count | INTEGER | default 0 |
budget_limit | NUMERIC(10,2) | default 320.00 (USD; 500/mo cap) |
budget_exceeded | BOOLEAN | default false — trigger-computed |
budget_percentage | NUMERIC(5,2) | default 0 — trigger-computed |
updated_at | DATETIME | default CURRENT_TIMESTAMP |
isaac_sim_config
Key/value settings, seeded with 11 rows on migration via INSERT … ON CONFLICT (config_key) DO NOTHING.
config_key | seeded config_value | meaning |
|---|---|---|
ami_id | '' | Isaac Sim AMI id (blank — set operationally) |
security_group_id | '' | AWS security group |
subnet_id | '' | AWS subnet |
instance_type | g6e.4xlarge | EC2 instance type |
auto_shutdown_hours | 2 | hours before auto-shutdown |
max_spot_price | 1.50 | max spot price/hour |
use_spot_instances | true | prefer spot |
monthly_budget_limit | 320 | monthly USD cap |
max_concurrent_instances | 3 | concurrency limit |
user_daily_limit_hours | 4 | per-user daily cap |
user_monthly_limit_hours | 20 | per-user monthly cap |
The 3 views
| View | Purpose |
|---|---|
active_isaac_instances | Instances with state IN ('launching','running'), joined to user/drone for username/drone_name, with computed runtime_hours (EPOCH(NOW()-launch_time)/3600) and minutes_until_shutdown. Ordered by launch_time DESC. |
isaac_monthly_usage_summary | isaac_sim_usage_tracking grouped by DATE_TRUNC('month', session_start) and user_id: session_count, total_hours, total_cost, avg_session_hours. |
isaac_budget_status | Last 12 isaac_sim_budget rows with remaining_budget = budget_limit - total_spent and a health status label: healthy (<50%), warning (<75%), critical (<90%), else exceeded. |
The 2 functions and 3 triggers
Two PL/pgSQL functions, wired to threeBEFORE UPDATE triggers:
| Function | Trigger | On table | Effect |
|---|---|---|---|
update_isaac_budget_percentage() | trigger_update_isaac_budget_percentage | isaac_sim_budget | Sets budget_percentage = (total_spent / budget_limit) * 100 and budget_exceeded = (budget_percentage >= 100) |
update_updated_at_column() | trigger_update_isaac_instances_updated_at | isaac_sim_instances | Sets updated_at = NOW() |
update_updated_at_column() | trigger_update_isaac_config_updated_at | isaac_sim_config | Sets updated_at = NOW() |
The only consumer: InstanceSchedulerService
Every read and write of these tables goes throughsrc/service/instance_scheduler_service.py, which holds a raw psycopg2 connection (self.db) and drives it directly — self.db.cursor(), cursor.execute(sql, params), self.db.commit(), self.db.rollback(). It never touches the SQLAlchemy db from src/connector/db_connection.py.
Its intended behaviour is an async loop that runs every 60 s:
The service also exposes extend_instance_runtime(instance_id, hours) (auto_shutdown_time += INTERVAL) and get_scheduler_stats() (state counts + upcoming-shutdown count). Cost is estimated in code at ~$0.90/hour plus $0.10 storage (instance_scheduler_service.py:206, :271).
Timezone mismatch (latent)
The table columns are naiveDateTime() (no timezone=True), so psycopg2 returns naive datetime objects, but the scheduler compares them against datetime.now(timezone.utc) (aware) at instance_scheduler_service.py:136 and subtracts them at :270. Mixing aware and naive datetimes raises TypeError in Python. This is masked today only because the service is not running; anyone wiring it in must normalize timezones (or make the columns TIMESTAMPTZ) first.
What a refactor must preserve
If you migrate the data layer (new ORM, schema rebuild, or “clean up dead code”), treat the Isaac Sim objects as a unit. Recreating only the tables silently drops the views and the budget math.All 4 tables with their exact FK actions
isaac_sim_instances (user_id CASCADE, drone_id SET NULL), isaac_sim_usage_tracking (both CASCADE), isaac_sim_budget, isaac_sim_config — plus every index (idx_isaac_instances_*, idx_isaac_usage_*, idx_isaac_budget_month).The 3 views verbatim
active_isaac_instances, isaac_monthly_usage_summary, isaac_budget_status. They quote "user" (reserved word) — keep the quoting.Both functions and all 3 triggers
update_isaac_budget_percentage() + update_updated_at_column(), and the three BEFORE UPDATE triggers. Without the budget trigger, budget_percentage/budget_exceeded are never populated by anything.The 11 seed rows in isaac_sim_config
Re-seed with
ON CONFLICT (config_key) DO NOTHING so a re-run is idempotent.The migration’s own
downgrade() is the canonical teardown order: DROP VIEW (3) → DROP TRIGGER (3) → DROP FUNCTION (2) → DROP TABLE (config, budget, usage_tracking, instances). Follow it if you ever hand-write a replacement.Related pages
Database Schema Overview
The ORM-backed tables these Isaac Sim objects deliberately bypass.
Migrations, DB Connection & Dev Mode
The Alembic chain, DB URI/pool config, and why db.create_all() misses these objects.
Platform Services (incl. Isaac Sim)
IsaacSimService, the boto3 single-instance manager behind the live routes.
Isaac Sim API
The /api/v1/isaac-sim/* HTTP endpoints (which do not use these tables).

