data-model-inventory.md
CODITECT Multi-Tenant SaaS Platform - Complete Model Inventory
Generated: 2025-12-30 Purpose: Comprehensive inventory of all existing Django models Status: Production data model for pilot launch (December 24, 2025)
Table of Contents
Overview
The CODITECT backend consists of 11 Django apps with 40 models total covering:
- User Management - Authentication, roles, OAuth
- Multi-Tenancy - Organization isolation and licensing
- Commerce - Products, carts, orders, entitlements
- Subscriptions - Billing, invoices, usage metrics
- Permissions - RBAC (role-based access control)
- Context Sync - Cross-device Claude Code session synchronization
- Licenses - Floating license management
- Workstations - Cloud development environments (GCP Workstations)
- Repositories - Multi-tenant Git hosting (Gitea integration)
Architecture Pattern
- Multi-Tenant Isolation: Django-multitenant with TenantModel mixin (automatic query filtering)
- Soft Deletes: deleted_at timestamps (not hard deletes)
- UUID Primary Keys: All models use UUIDs (except global catalog models)
- Timestamps: created_at, updated_at on all models
- Indexes: Strategic indexes for query performance
- Constraints: Unique constraints for business rules
Apps Summary
| App | Models Count | Primary Purpose | Multi-Tenant |
|---|---|---|---|
| users | 1 | User authentication (email, OAuth, roles) | Via FK to Tenant |
| tenants | 1 | Organization-level data (licenses, seats) | TenantModel (root) |
| organizations | 4 | Teams, memberships, projects | TenantModel |
| commerce | 5 | Products, carts, orders, entitlements | Mixed (Product global, rest tenant-scoped) |
| subscriptions | 3 | Billing, invoices, usage tracking | TenantModel |
| permissions | 1 | RBAC permission definitions | Global (no tenant) |
| context | 3 | Claude Code session sync (messages, cursors) | TenantModel |
| licenses | 2 | Floating licenses, sessions | Via FK to Tenant |
| workstations | 6 | Cloud development environments | Via tenant_id field |
| repositories | 7 | Gitea multi-tenant Git hosting | Via organization FK |
| core | 0 | Base app (no models) | N/A |
| TOTAL | 40 |
Detailed Model Inventory
Users App
Location: backend/users/models.py
Models: 1
User
Purpose: Custom user model extending Django's AbstractUser with tenant support.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique user identifier |
username | CharField(150) | Unique | Django AbstractUser field |
first_name | CharField(150) | Django AbstractUser field | |
last_name | CharField(150) | Django AbstractUser field | |
email | EmailField | Unique | Globally unique email |
password | CharField(128) | Django AbstractUser field (hashed) | |
is_staff | BooleanField | Django admin access | |
is_active | BooleanField | Account active status | |
is_superuser | BooleanField | Django superuser flag | |
date_joined | DateTimeField | Django AbstractUser field | |
last_login | DateTimeField | Nullable | Django AbstractUser field |
tenant | ForeignKey(Tenant) | CASCADE | Tenant this user belongs to |
role | CharField(20) | 'owner', 'admin', 'member', 'guest' | |
oauth_provider | CharField(50) | Nullable | 'google', 'github' |
oauth_sub | CharField(255) | Nullable | OAuth subject identifier |
updated_at | DateTimeField | auto_now | Last update timestamp |
Indexes:
tenant + emailoauth_provider + oauth_subrole
Unique Constraints:
(tenant, email)- Email unique per tenant
Business Rules:
- Email globally unique AND unique per tenant
- role determines permissions (via Permission model)
- OAuth users have oauth_provider + oauth_sub populated
Methods:
is_tenant_owner()- Check if role='owner'is_tenant_admin()- Check if role in ['owner', 'admin']can_manage_users()- Permission checkcan_manage_licenses()- Permission check
Tenants App
Location: backend/tenants/models.py
Models: 1
Tenant
Purpose: Root tenant (organization) model - represents a customer organization.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique tenant identifier |
name | CharField(255) | Organization name | |
slug | SlugField(100) | Unique | URL-safe identifier (e.g., 'acme-corp') |
email | EmailField | Primary contact email | |
license_tier | CharField(20) | 'trial', 'starter', 'professional', 'enterprise' | |
seats_total | PositiveIntegerField | default=1 | Concurrent seat limit |
is_active | BooleanField | default=True | Account active status |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
stripe_customer_id | CharField(255) | Nullable | Stripe customer ID |
stripe_subscription_id | CharField(255) | Nullable | Stripe subscription ID |
Inherits From: TenantModel (django-multitenant)
Indexes:
slugemailis_active
Business Rules:
- slug globally unique
- seats_total determines concurrent session limit
- Stripe IDs populated after checkout completion
Methods:
get_active_seat_count()- Count active sessionshas_available_seats()- Check seat availabilityget_seat_utilization()- Percentage of seats used
Organizations App
Location: backend/organizations/models.py
Models: 4
MembershipRole (Enum)
Purpose: Enum for membership roles.
Values:
OWNER- 'owner'ADMIN- 'admin'MEMBER- 'member'VIEWER- 'viewer'
Membership
Purpose: User-to-Organization membership with role.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique membership identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
user | TenantForeignKey(User) | CASCADE | User in this organization |
role | CharField(20) | MembershipRole choice | |
joined_at | DateTimeField | default=now | When user joined |
revoked_at | DateTimeField | Nullable | When membership was revoked |
Inherits From: TenantModel
Indexes:
(tenant, user, role)whererevoked_at IS NULL
Unique Constraints:
(user, tenant)- User can only have one role per org
Methods:
is_active()- Check if revoked_at is None
Team
Purpose: Team within an organization.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique team identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
name | CharField(255) | Team name | |
description | TextField | Team description | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Inherits From: TenantModel
Unique Constraints:
(tenant, name)- Team name unique per org
TeamMembership
Purpose: User-to-Team membership.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique membership identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
team | TenantForeignKey(Team) | CASCADE | Team |
user | TenantForeignKey(User) | CASCADE | User |
joined_at | DateTimeField | default=now | When user joined team |
Inherits From: TenantModel
Unique Constraints:
(team, user)- User can only belong to a team once
Project
Purpose: Project within an organization.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique project identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
name | CharField(255) | Project name | |
description | TextField | Project description | |
metadata | JSONField | default=dict | Arbitrary metadata |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
deleted_at | DateTimeField | Nullable | Soft delete timestamp |
Inherits From: TenantModel
Indexes:
(tenant, -created_at)wheredeleted_at IS NULL
Unique Constraints:
(tenant, name)wheredeleted_at IS NULL
Methods:
soft_delete()- Set deleted_at to now
Commerce App
Location: backend/commerce/models.py
Models: 5
Product
Purpose: Global product catalog (NOT tenant-specific).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique product identifier |
slug | SlugField(50) | Unique | Product slug ('core', 'dms', 'workflow', 'enterprise') |
name | CharField(255) | Product name | |
description | TextField | Product description | |
product_type | CharField(20) | 'base', 'addon', 'bundle' | |
price_cents | IntegerField | Price in cents (e.g., 4900 = $49.00) | |
currency | CharField(3) | default='usd' | Currency code |
billing_interval | CharField(10) | 'month', 'year' | |
stripe_price_id | CharField(255) | Unique, nullable | Stripe Price ID |
features | JSONField | default=list | List of feature strings |
requires | JSONField | default=list | List of required product slugs |
subdomain | CharField(255) | Nullable | Product subdomain (e.g., 'dms.coditect.ai') |
active | BooleanField | default=True | Product active in catalog |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Indexes:
slugwhereactive=Trueproduct_typewhereactive=True
Business Rules:
- Global catalog (all tenants see same products)
- requires field enforces dependencies (e.g., DMS requires Core)
Methods:
get_dependencies()- Return list of required slugs
CODITECT Products:
- core - CODITECT Core (base, $49/mo)
- dms - CODITECT DMS (addon, $29/mo, requires core)
- workflow - Workflow Analyzer (addon, $19/mo, requires core)
- enterprise - Enterprise Bundle (bundle, $149/mo, all products)
Cart
Purpose: Shopping cart (guest or authenticated).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique cart identifier |
user | ForeignKey(User) | CASCADE, nullable | Authenticated user |
session_id | CharField(255) | Unique, nullable | Guest session ID |
items | JSONField | default=list | List of {product_id, quantity, price_cents} |
total_cents | IntegerField | default=0 | Total cart value in cents |
currency | CharField(3) | default='usd' | Currency code |
status | CharField(20) | 'active', 'checkout', 'completed', 'abandoned' | |
expires_at | DateTimeField | Cart expiration (24 hours from last update) | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Indexes:
userwherestatus='active'session_idwherestatus='active'expires_atwherestatus='active'
Business Rules:
- One active cart per user OR session_id
- Cart expires after 24 hours of inactivity
- Must validate dependencies before checkout
Methods:
add_item(product, quantity)- Add product to cartremove_item(product_id)- Remove product from cart_recalculate_total()- Recalculate total_centsis_expired()- Check if cart expiredvalidate_dependencies()- Check product requirements
Order
Purpose: Completed purchase order.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique order identifier |
tenant | ForeignKey(Tenant) | SET_NULL, nullable | Organization (may be NULL pre-assignment) |
user | ForeignKey(User) | RESTRICT | Purchasing user |
stripe_payment_intent_id | CharField(255) | Unique, nullable | Stripe PaymentIntent ID |
stripe_checkout_session_id | CharField(255) | Unique, nullable | Stripe Checkout Session ID |
payment_method | CharField(50) | 'stripe_checkout', 'google_pay', 'apple_pay' | |
total_cents | IntegerField | Total order value in cents | |
currency | CharField(3) | default='usd' | Currency code |
status | CharField(20) | 'pending', 'processing', 'paid', 'failed', 'refunded' | |
line_items | JSONField | default=list | List of {product_id, product_slug, quantity, price_cents} |
billing_details | JSONField | Nullable | Billing address, name, etc. |
paid_at | DateTimeField | Nullable | Payment completion timestamp |
created_at | DateTimeField | auto_now_add | Order creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Inherits From: TenantModel
Indexes:
usertenantstatuswherestatus IN ('pending', 'processing')stripe_payment_intent_id
Methods:
mark_paid()- Set status='paid', paid_at=now
Entitlement
Purpose: Product access entitlement for an organization.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique entitlement identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
product | ForeignKey(Product) | RESTRICT | Product granted |
order | ForeignKey(Order) | SET_NULL, nullable | Originating order |
subscription | ForeignKey(Subscription) | SET_NULL, nullable | Linked subscription |
status | CharField(20) | 'active', 'expired', 'suspended', 'cancelled' | |
starts_at | DateTimeField | default=now | Entitlement start time |
expires_at | DateTimeField | Nullable | Entitlement expiration |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Inherits From: TenantModel
Indexes:
tenantwherestatus='active'productwherestatus='active'expires_atwhereexpires_at IS NOT NULL AND status='active'
Unique Constraints:
(tenant, product)- One entitlement per product per org
Methods:
is_valid()- Check if status='active' AND not expiredsuspend()- Set status='suspended'cancel()- Set status='cancelled'
Subscriptions App
Location: backend/subscriptions/models.py
Models: 3
Subscription
Purpose: Stripe subscription management.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique subscription identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
stripe_subscription_id | CharField(255) | Unique | Stripe Subscription ID |
stripe_customer_id | CharField(255) | Stripe Customer ID | |
status | CharField(20) | 'active', 'past_due', 'canceled', 'incomplete', 'trialing', 'unpaid' | |
tier | CharField(20) | 'free', 'pro', 'enterprise' | |
max_workstations | IntegerField | default=1 | Workstation quota |
max_users | IntegerField | default=1 | User quota |
current_period_start | DateTimeField | Billing period start | |
current_period_end | DateTimeField | Billing period end | |
canceled_at | DateTimeField | Nullable | Cancellation timestamp |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Inherits From: TenantModel
Indexes:
current_period_endwherestatus='active'
Methods:
is_active()- Check if status='active'get_remaining_workstation_quota(current_count)- Calculate remaining slotsget_remaining_user_quota(current_count)- Calculate remaining slots
Invoice
Purpose: Invoice records synced from Stripe.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique invoice identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
subscription | TenantForeignKey(Subscription) | CASCADE | Linked subscription |
stripe_invoice_id | CharField(255) | Unique | Stripe Invoice ID |
amount_cents | IntegerField | Invoice total in cents | |
currency | CharField(3) | default='usd' | Currency code |
status | CharField(20) | 'draft', 'open', 'paid', 'void', 'uncollectible' | |
description | TextField | Invoice description | |
invoice_pdf_url | URLField | Nullable | Stripe-hosted PDF |
hosted_invoice_url | URLField | Nullable | Stripe-hosted invoice page |
created_at | DateTimeField | auto_now_add | Creation timestamp |
paid_at | DateTimeField | Nullable | Payment timestamp |
Inherits From: TenantModel
Indexes:
subscriptionstripe_invoice_id
UsageMetric
Purpose: Resource usage tracking for quota enforcement.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique metric identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
metric_type | CharField(50) | 'workstations', 'users', 'storage_gb', 'api_calls', 'context_messages', 'sync_operations' | |
current_value | IntegerField | default=0 | Current usage count |
limit_value | IntegerField | default=0 | Quota limit |
period_start | DateTimeField | Tracking period start | |
period_end | DateTimeField | Tracking period end | |
recorded_at | DateTimeField | auto_now | Last update timestamp |
created_at | DateTimeField | auto_now_add | Creation timestamp |
Inherits From: TenantModel
Indexes:
(tenant, metric_type)period_end
Unique Constraints:
(tenant, metric_type, period_start)
Methods:
get_usage_percentage()- Calculate usage/limit * 100is_at_limit()- Check if current_value >= limit_valueincrement(amount)- Increment current_value
Permissions App
Location: backend/permissions/models.py
Models: 1
Permission
Purpose: RBAC permission definitions (role, resource, action tuples).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique permission identifier |
role | CharField(20) | 'owner', 'admin', 'member', 'viewer' | |
resource | CharField(50) | Resource type enum (organization, team, project, etc.) | |
action | CharField(20) | Action enum (create, read, update, delete, etc.) | |
description | CharField(255) | Human-readable description | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
Indexes:
roleresource
Unique Constraints:
(role, resource, action)- One permission per combination
Class Methods:
check_permission(role, resource, action)- Check if permission existsget_role_permissions(role)- Get all permissions for a roleseed_default_permissions()- Seed default RBAC rules
Resources Enum:
- organization, team, project, workstation, repository, subscription, user, membership, context, entitlement, order, invoice
Actions Enum:
- create, read, update, delete, list, manage, invite, remove, start, stop, connect, sync
Context App
Location: backend/context/models.py
Models: 3
ContextMessage
Purpose: Claude Code session messages (sync across devices).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique message identifier |
tenant | TenantForeignKey(Tenant) | CASCADE | Organization |
user_id | CharField(63) | User identifier | |
session_id | CharField(255) | Session identifier | |
project_path | CharField(1000) | Project directory path | |
message_type | CharField(50) | 'human', 'assistant', 'tool_use', 'tool_result', 'summary' | |
role | CharField(50) | default='user' | Message role |
content | TextField | Message content | |
content_hash | CharField(64) | Unique | SHA256 hash (tenant_id:session_id:content) |
message_timestamp | DateTimeField | Original client timestamp | |
synced_at | DateTimeField | auto_now_add | Server sync timestamp |
sync_cursor | BigIntegerField | Unique, nullable | Auto-increment cursor (PostgreSQL sequence) |
client_version | CharField(50) | CODITECT client version | |
hardware_id | CharField(255) | Device hardware ID |
Inherits From: TenantModel
Indexes:
(tenant, sync_cursor)(tenant, user_id, synced_at)(session_id, synced_at)
Business Rules:
- content_hash prevents duplicate messages
- sync_cursor enables cursor-based polling
- Immutable once created
SyncCursor
Purpose: Per-device sync cursor tracking.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique cursor identifier |
tenant | TenantForeignKey(Tenant) | CASCADE | Organization |
user_id | CharField(63) | User identifier | |
hardware_id | CharField(255) | Device hardware ID | |
last_cursor | BigIntegerField | default=0 | Last synced cursor value |
last_sync_at | DateTimeField | auto_now | Last sync timestamp |
client_version | CharField(50) | CODITECT client version | |
device_name | CharField(255) | Device name |
Inherits From: TenantModel
Unique Together:
(tenant, user_id, hardware_id)
SyncStats
Purpose: Aggregated sync statistics for monitoring.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique stats identifier |
tenant | TenantForeignKey(Tenant) | CASCADE | Organization |
user_id | CharField(63) | User identifier | |
total_messages | BigIntegerField | default=0 | Total messages synced |
total_sessions | IntegerField | default=0 | Total unique sessions |
total_devices | IntegerField | default=0 | Total devices registered |
total_bytes | BigIntegerField | default=0 | Total bytes synced |
last_push_at | DateTimeField | Nullable | Last message push timestamp |
last_pull_at | DateTimeField | Nullable | Last message pull timestamp |
period_start | DateField | Stats period start | |
period_end | DateField | Stats period end |
Inherits From: TenantModel
Unique Together:
(tenant, user_id, period_start)
Licenses App
Location: backend/licenses/models.py
Models: 2
License
Purpose: License allocation for a tenant (floating concurrent licensing).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique license identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
license_key | CharField(255) | Unique | Signed license key (Cloud KMS) |
license_tier | CharField(20) | 'trial', 'starter', 'professional', 'enterprise' | |
seats_total | PositiveIntegerField | default=1 | Concurrent seat limit |
issued_at | DateTimeField | auto_now_add | Issuance timestamp |
expires_at | DateTimeField | Expiration timestamp | |
is_active | BooleanField | default=True | License active status |
is_suspended | BooleanField | default=False | License suspended (payment issues) |
notes | TextField | Internal notes | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Indexes:
(tenant, is_active)license_keyexpires_at
Methods:
is_expired()- Check if now > expires_atis_valid()- Check active AND not suspended AND not expiredget_active_sessions()- Get sessions with status='active'get_seats_used()- Count active sessionshas_available_seats()- Check seats_used < seats_totalget_seat_utilization()- Percentage of seats useddays_until_expiration()- Days remaining
Session
Purpose: Active CODITECT session consuming a license seat.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique session identifier |
tenant | ForeignKey(Tenant) | CASCADE | Organization |
license | ForeignKey(License) | CASCADE | License being used |
user | ForeignKey(User) | SET_NULL, nullable | User (optional) |
hardware_id | CharField(255) | Unique hardware fingerprint | |
hostname | CharField(255) | Machine hostname | |
status | CharField(20) | 'active', 'expired', 'released', 'zombie' | |
last_heartbeat_at | DateTimeField | auto_now_add | Last heartbeat timestamp |
heartbeat_count | PositiveIntegerField | default=0 | Total heartbeats received |
started_at | DateTimeField | auto_now_add | Session start timestamp |
ended_at | DateTimeField | Nullable | Session end timestamp |
client_version | CharField(50) | CODITECT client version | |
ip_address | GenericIPAddressField | Nullable | Client IP address |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Indexes:
(tenant, license, status)hardware_idlast_heartbeat_at(status, last_heartbeat_at)
Unique Constraints:
(license, hardware_id, status)wherestatus='active'- One active session per hardware per license
Methods:
is_active()- Check status='active'is_heartbeat_alive(timeout_seconds=360)- Check heartbeat within TTLmark_zombie()- Set status='zombie'release()- Set status='released', ended_at=nowrecord_heartbeat()- Update last_heartbeat_at, increment countget_session_duration()- Duration in secondsget_session_duration_formatted()- Human-readable (e.g., '2h 15m')
Workstations App
Location: backend/workstations/models.py
Models: 6
WorkstationConfig
Purpose: Workstation configuration tier (GCP machine specs).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique config identifier |
name | CharField(63) | Unique | GCP config name |
display_name | CharField(255) | Human-readable name | |
tier | CharField(20) | 'standard', 'power', 'ai' | |
machine_type | CharField(50) | default='e2-standard-4' | GCP machine type |
vcpu_count | IntegerField | default=4 | vCPU count |
memory_gb | IntegerField | default=16 | Memory in GB |
boot_disk_gb | IntegerField | default=50 | Boot disk size |
persistent_disk_gb | IntegerField | default=100 | Persistent disk size |
has_gpu | BooleanField | default=False | GPU attached |
gpu_type | CharField(50) | Nullable | GPU type (e.g., 'nvidia-tesla-t4') |
hourly_cost_usd | DecimalField(10,4) | default=0.08 | Estimated hourly cost |
gcp_config_id | CharField(255) | GCP resource ID | |
gcp_cluster_id | CharField(255) | GCP cluster ID | |
region | CharField(50) | default='us-central1' | GCP region |
is_active | BooleanField | default=True | Config active |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Workstation
Purpose: Dedicated workstation instance (one per user per tenant).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique workstation identifier |
workstation_id | CharField(63) | Unique | GCP workstation ID (ws-{tenant_id}-{user_id}) |
display_name | CharField(255) | Human-readable name | |
tenant_id | CharField(63) | Tenant identifier | |
user_id | CharField(63) | User identifier | |
user_email | EmailField | User email | |
config | ForeignKey(WorkstationConfig) | PROTECT | Configuration tier |
status | CharField(20) | 'CREATING', 'STARTING', 'RUNNING', 'STOPPING', 'STOPPED', 'DELETING', 'DELETED', 'ERROR' | |
status_message | TextField | Status details | |
last_status_check | DateTimeField | Nullable | Last status poll |
gcp_resource_name | CharField(500) | GCP resource name | |
gcp_host_uri | URLField(500) | Connection URI | |
total_running_seconds | BigIntegerField | default=0 | Cumulative runtime |
current_session_start | DateTimeField | Nullable | Current session start |
last_activity_at | DateTimeField | Nullable | Last activity timestamp |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
deleted_at | DateTimeField | Nullable | Soft delete timestamp |
Indexes:
(tenant_id, status)(user_id, status)
Unique Together:
(tenant_id, user_id)- One workstation per user per tenant
Properties:
is_running- Check status='RUNNING'estimated_monthly_cost- Based on config hourly_cost_usd * 80 hours/month
Methods:
start_session()- Set status='RUNNING', current_session_start=nowend_session()- Accumulate runtime, set status='STOPPED'
SharedWorkstation
Purpose: Multi-user shared workstation (cost optimization).
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique workstation identifier |
workstation_id | CharField(63) | Unique | GCP workstation ID |
display_name | CharField(255) | Human-readable name | |
tenant_id | CharField(63) | Tenant identifier | |
sharing_mode | CharField(20) | 'dedicated', 'shared', 'pool' | |
max_concurrent_users | IntegerField | default=5 | Max simultaneous users |
current_user_count | IntegerField | default=0 | Currently connected users |
config | ForeignKey(WorkstationConfig) | PROTECT | Configuration tier |
status | CharField(20) | 'CREATING', 'RUNNING', 'STOPPED', 'ERROR' | |
gcp_resource_name | CharField(500) | GCP resource name | |
gcp_host_uri | URLField(500) | Connection URI | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
WorkstationUserAccess
Purpose: Maps users to workstations with access control.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique access identifier |
user_id | CharField(63) | User identifier | |
user_email | EmailField | User email | |
tenant_id | CharField(63) | Tenant identifier | |
dedicated_workstation | ForeignKey(Workstation) | SET_NULL, nullable | Dedicated workstation |
shared_workstations | ManyToManyField(SharedWorkstation) | Shared workstations | |
access_level | CharField(20) | 'user', 'developer', 'admin' | |
role | CharField(100) | User role (e.g., 'frontend', 'backend') | |
linux_username | CharField(32) | Linux username in workstation | |
linux_uid | IntegerField | Nullable | Linux UID for isolation |
ssh_public_key | TextField | SSH public key | |
home_directory | CharField(255) | Persistent home directory path | |
home_storage_gb | IntegerField | default=10 | Home directory quota |
default_workstation_id | CharField(63) | Default workstation to launch | |
is_active | BooleanField | default=True | Access active |
last_login_at | DateTimeField | Nullable | Last login timestamp |
last_workstation_id | CharField(63) | Last workstation used | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Indexes:
user_id(tenant_id, is_active)
Unique Together:
(tenant_id, user_id)
Methods:
get_available_workstations()- List all accessible workstations
WorkstationSession
Purpose: Tracks individual user sessions on workstations.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique session identifier |
session_id | CharField(63) | Unique | Session identifier |
user_access | ForeignKey(WorkstationUserAccess) | CASCADE | User access record |
workstation | ForeignKey(Workstation) | CASCADE, nullable | Dedicated workstation |
shared_workstation | ForeignKey(SharedWorkstation) | CASCADE, nullable | Shared workstation |
state | CharField(20) | 'connecting', 'active', 'idle', 'disconnected' | |
started_at | DateTimeField | auto_now_add | Session start |
last_activity_at | DateTimeField | auto_now | Last activity |
ended_at | DateTimeField | Nullable | Session end |
client_ip | GenericIPAddressField | Nullable | Client IP |
user_agent | CharField(500) | User agent |
Indexes:
(state, started_at)
WorkstationUsageRecord
Purpose: Detailed usage records for billing.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | UUIDField | PK, default=uuid4 | Unique record identifier |
workstation | ForeignKey(Workstation) | CASCADE | Workstation |
started_at | DateTimeField | Session start | |
stopped_at | DateTimeField | Nullable | Session stop |
duration_seconds | IntegerField | default=0 | Session duration |
config_hourly_rate | DecimalField(10,4) | Hourly rate at session time | |
calculated_cost_usd | DecimalField(10,4) | default=0 | Calculated cost |
tenant_id | CharField(63) | Tenant for billing | |
created_at | DateTimeField | auto_now_add | Record creation |
Indexes:
(tenant_id, started_at)(workstation, started_at)
Methods:
calculate_cost()- Calculate cost based on duration and rate
Repositories App
Location: backend/repositories/models.py
Models: 7
GiteaOrganization
Purpose: Gitea organization for a tenant (multi-tenant Git hosting).
| Field | Type | Constraints | Description |
|---|---|---|---|
tenant_id | CharField(63) | Unique | Tenant identifier |
gitea_org_name | CharField(63) | Unique | Gitea org name (org-{tenant_id}) |
gitea_org_id | IntegerField | Nullable | Gitea internal org ID |
display_name | CharField(255) | Human-readable name | |
description | TextField | Organization description | |
visibility | CharField(20) | 'private', 'limited', 'public' | |
repo_limit | IntegerField | default=-1 | Max repositories (-1=unlimited) |
member_limit | IntegerField | default=-1 | Max members (-1=unlimited) |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
synced_at | DateTimeField | Nullable | Last Gitea API sync |
Repository
Purpose: Git repository within a tenant organization.
| Field | Type | Constraints | Description |
|---|---|---|---|
organization | ForeignKey(GiteaOrganization) | CASCADE | Parent organization |
name | CharField(255) | Repository name | |
gitea_repo_id | IntegerField | Nullable | Gitea internal repo ID |
full_name | CharField(511) | Full path (org/repo) | |
description | TextField | Repository description | |
default_branch | CharField(255) | default='main' | Default branch |
visibility | CharField(20) | 'private', 'internal', 'public' | |
has_issues | BooleanField | default=False | Issues enabled |
has_wiki | BooleanField | default=False | Wiki enabled |
has_pull_requests | BooleanField | default=True | PRs enabled |
size_kb | BigIntegerField | default=0 | Repository size |
stars_count | IntegerField | default=0 | Stars count |
forks_count | IntegerField | default=0 | Forks count |
open_issues_count | IntegerField | default=0 | Open issues |
open_pr_count | IntegerField | default=0 | Open PRs |
clone_url_https | URLField | HTTPS clone URL | |
clone_url_ssh | CharField(511) | SSH clone URL | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
synced_at | DateTimeField | Nullable | Last Gitea API sync |
last_activity_at | DateTimeField | Nullable | Last push/activity |
is_archived | BooleanField | default=False | Repository archived |
archived_at | DateTimeField | Nullable | Archive timestamp |
Indexes:
(organization, name)full_name
Unique Together:
(organization, name)
RepositoryAccessToken
Purpose: Short-lived access tokens for repository operations.
| Field | Type | Constraints | Description |
|---|---|---|---|
token_id | CharField(63) | Unique | Token identifier |
user_id | CharField(63) | User identifier | |
repository | ForeignKey(Repository) | CASCADE, nullable | Specific repository (or org-wide) |
organization | ForeignKey(GiteaOrganization) | CASCADE | Parent organization |
gitea_token_id | IntegerField | Nullable | Gitea internal token ID |
name | CharField(255) | Token name in Gitea | |
scope | CharField(20) | 'read', 'write', 'admin' | |
token_hash | CharField(255) | SHA-256 hash of token | |
token_prefix | CharField(10) | First 8 chars (for identification) | |
expires_at | DateTimeField | Token expiration | |
is_revoked | BooleanField | default=False | Token revoked |
revoked_at | DateTimeField | Nullable | Revocation timestamp |
revoked_reason | CharField(255) | Revocation reason | |
created_at | DateTimeField | auto_now_add | Creation timestamp |
last_used_at | DateTimeField | Nullable | Last use timestamp |
use_count | IntegerField | default=0 | Usage count |
Indexes:
user_idexpires_attoken_prefix
RepositoryMirror
Purpose: GitHub/GitLab/Bitbucket mirror configuration for bidirectional sync.
| Field | Type | Constraints | Description |
|---|---|---|---|
repository | OneToOneField(Repository) | CASCADE | Local Gitea repository |
mirror_type | CharField(20) | 'github', 'gitlab', 'bitbucket' | |
remote_url | URLField | Remote repository URL | |
remote_owner | CharField(255) | Remote owner/org | |
remote_name | CharField(255) | Remote repo name | |
auth_token_secret | CharField(255) | Secret Manager path for token | |
direction | CharField(20) | 'pull', 'push', 'bidirectional' | |
sync_interval_seconds | IntegerField | default=300 | Sync interval (5 min) |
is_active | BooleanField | default=True | Mirroring active |
last_sync_at | DateTimeField | Nullable | Last sync timestamp |
last_sync_status | CharField(20) | 'pending', 'syncing', 'success', 'failed' | |
last_sync_error | TextField | Error message | |
sync_count | IntegerField | default=0 | Total sync attempts |
failure_count | IntegerField | default=0 | Failed sync attempts |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
Methods:
sync()- Trigger mirror sync (Celery task)
TenantGitQuota
Purpose: Git storage and repository quota tracking.
| Field | Type | Constraints | Description |
|---|---|---|---|
organization | OneToOneField(GiteaOrganization) | CASCADE | Gitea organization |
tier | CharField(20) | 'free', 'starter', 'professional', 'enterprise' | |
repo_limit | IntegerField | default=3 | Max repositories |
storage_limit_gb | IntegerField | default=1 | Storage limit in GB |
github_mirror_enabled | BooleanField | default=False | Mirroring enabled |
repo_count | IntegerField | default=0 | Current repository count |
storage_used_mb | BigIntegerField | default=0 | Storage usage in MB |
mirror_count | IntegerField | default=0 | Active mirrors |
monthly_cost_usd | DecimalField(10,2) | default=0 | Base monthly cost |
overage_cost_usd | DecimalField(10,2) | default=0 | Overage charges |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
usage_synced_at | DateTimeField | Nullable | Last usage calculation |
Properties:
storage_used_gb- storage_used_mb / 1024storage_percent- (storage_used_gb / storage_limit_gb) * 100repo_percent- (repo_count / repo_limit) * 100
Methods:
is_over_quota()- Check if over any quotacalculate_cost()- Calculate monthly cost + overages
RepositoryWebhook
Purpose: Webhooks for CI/CD and external integrations.
| Field | Type | Constraints | Description |
|---|---|---|---|
repository | ForeignKey(Repository) | CASCADE | Repository |
gitea_hook_id | IntegerField | Nullable | Gitea internal hook ID |
name | CharField(255) | Webhook name | |
url | URLField | Webhook target URL | |
secret_hash | CharField(255) | SHA-256 hash of secret | |
content_type | CharField(20) | 'json', 'form' | |
events | JSONField | default=list | List of events to trigger |
is_active | BooleanField | default=True | Webhook active |
created_at | DateTimeField | auto_now_add | Creation timestamp |
updated_at | DateTimeField | auto_now | Last update timestamp |
last_delivery_at | DateTimeField | Nullable | Last delivery timestamp |
last_delivery_status | IntegerField | Nullable | Last HTTP status |
delivery_count | IntegerField | default=0 | Total deliveries |
failure_count | IntegerField | default=0 | Failed deliveries |
Core App
Location: backend/core/models.py
Models: 0
Purpose: Base application with no models (shared utilities, base views, etc.)
Multi-Tenant Architecture
django-multitenant Integration
Pattern: TenantModel mixin for automatic query filtering by tenant.
Apps Using TenantModel:
- tenants (Tenant - root model)
- organizations (Membership, Team, TeamMembership, Project)
- commerce (Order, Entitlement)
- subscriptions (Subscription, Invoice, UsageMetric)
- context (ContextMessage, SyncCursor, SyncStats)
Apps Using ForeignKey to Tenant:
- users (User - special case: no TenantModel due to AbstractUser MRO conflicts)
- licenses (License, Session)
Apps Using tenant_id CharField:
- workstations (all models)
- repositories (via GiteaOrganization.tenant_id)
Global Models (No Tenant):
- commerce.Product (global catalog)
- permissions.Permission (global RBAC rules)
- workstations.WorkstationConfig (global config tiers)
Usage Pattern
from django_multitenant.utils import set_current_tenant
# Set tenant context (middleware does this automatically)
set_current_tenant(tenant)
# All subsequent TenantModel queries auto-filtered by tenant
projects = Project.objects.all() # Automatically filtered to current tenant
Database Tables Summary
| Table | Model | App | Tenant-Scoped |
|---|---|---|---|
users | User | users | Via FK |
tenants | Tenant | tenants | Root |
memberships | Membership | organizations | TenantModel |
teams | Team | organizations | TenantModel |
team_memberships | TeamMembership | organizations | TenantModel |
projects | Project | organizations | TenantModel |
products | Product | commerce | Global |
carts | Cart | commerce | Global (user or session) |
orders | Order | commerce | TenantModel |
entitlements | Entitlement | commerce | TenantModel |
subscriptions | Subscription | subscriptions | TenantModel |
invoices | Invoice | subscriptions | TenantModel |
usage_metrics | UsageMetric | subscriptions | TenantModel |
permissions | Permission | permissions | Global |
context_messages | ContextMessage | context | TenantModel |
context_sync_cursors | SyncCursor | context | TenantModel |
context_sync_stats | SyncStats | context | TenantModel |
licenses | License | licenses | Via FK |
sessions | Session | licenses | Via FK |
workstation_configs | WorkstationConfig | workstations | Global |
workstations | Workstation | workstations | Via tenant_id |
shared_workstations | SharedWorkstation | workstations | Via tenant_id |
workstation_user_access | WorkstationUserAccess | workstations | Via tenant_id |
workstation_sessions | WorkstationSession | workstations | Via user_access FK |
workstation_usage_records | WorkstationUsageRecord | workstations | Via tenant_id |
gitea_organizations | GiteaOrganization | repositories | Via tenant_id |
repositories | Repository | repositories | Via organization FK |
repository_access_tokens | RepositoryAccessToken | repositories | Via organization FK |
repository_mirrors | RepositoryMirror | repositories | Via repository FK |
tenant_git_quotas | TenantGitQuota | repositories | Via organization FK |
repository_webhooks | RepositoryWebhook | repositories | Via repository FK |
Total Tables: 31
Field Type Summary
| Field Type | Count | Common Usage |
|---|---|---|
| UUIDField | 40 | Primary keys (all models) |
| CharField | 200+ | Names, slugs, identifiers, statuses |
| TextField | 30+ | Descriptions, notes, content |
| ForeignKey | 60+ | Relationships |
| DateTimeField | 120+ | Timestamps (created_at, updated_at, etc.) |
| BooleanField | 40+ | Flags (is_active, is_suspended, etc.) |
| IntegerField | 50+ | Counts, quotas, limits |
| BigIntegerField | 10+ | Large counts (bytes, cursors) |
| JSONField | 15+ | Flexible data (metadata, line_items, features) |
| EmailField | 10+ | User emails |
| URLField | 15+ | Clone URLs, webhooks, hosted invoice URLs |
| DecimalField | 10+ | Money amounts (prices, costs) |
| GenericIPAddressField | 3 | IP addresses |
Index Strategy
Common Index Patterns:
- Foreign Keys - All FKs indexed by default
- Tenant Scoping -
(tenant, ...)composite indexes for multi-tenant queries - Status Filtering - Partial indexes with
WHERE status IN (...)conditions - Time-Range Queries - Indexes on timestamp fields (created_at, expires_at, etc.)
- Unique Business Rules - Unique constraints for data integrity
Examples:
(tenant, status)- Active records per tenantexpires_at WHERE status='active'- Find expiring records(tenant, user, role) WHERE revoked_at IS NULL- Active membershipsstripe_payment_intent_id- Fast Stripe webhook lookups
Validation Summary
✅ Multi-Tenant Compliance: All tenant-scoped models use TenantModel or FK to Tenant ✅ UUID Primary Keys: All models use UUIDs for distributed systems ✅ Timestamps: All models have created_at, updated_at ✅ Soft Deletes: Projects, workstations use deleted_at ✅ Indexes: Strategic indexes for query performance ✅ Constraints: Unique constraints enforce business rules ✅ Choices: Enums for controlled vocabularies (status, tier, role, etc.)
End of data-model-inventory.md