Skip to main content

Data model

GreekManage's persistent data lives in PostgreSQL across 23 Django apps and ~157 models. This page maps the core entities and shows how they connect.

Core entity diagram

Operational entities

Engagement entities

Program entities

Intelligence + audit

Notable invariants

Membership status drives platform access

Membership.status controls whether a user can sign in:

StatusPlatform access
pnm❌ no — applicants pre-bid
pledge✅ yes
undergraduate✅ yes (typical active)
associate✅ yes
alumni✅ yes
alumni_active✅ yes
alumni_lifetime✅ yes
inactive❌ no — preserved for history
disaffiliated❌ no — blocked, audit-logged

Code reference: apps/organizations/models.py Membership.PLATFORM_ACCESS_STATUSES.

Organization status cascades

Organization.is_active = False blocks all sign-ins to that tenant. Orgs are never deleted — only deactivated, since financial records must be retained.

One Membership per (User, Chapter)

A user can only be a member of a single chapter at a given time. Promoting from one chapter to another (e.g., transfer) creates a new Membership with the old one moved to inactive.

Officer ⇒ Membership.role transition

Promoting a member to officer changes Membership.role from member to officer (or president). Demotion is the reverse. There's no separate Officer table.

Encrypted columns

Several columns are stored encrypted via EncryptedTextField (Fernet, with MultiFernet for rotation):

ModelFieldWhy
Userphone_numberPII
SAMLConfigurationx509_certIdentity provider secret
OAuthConfigurationclient_secretIdentity provider secret
LinkedInAccountaccess_token, refresh_tokenThird-party tokens
BackupDestinationsecret_access_keyCloud secret
OrgPaymentProcessor(provider creds)PCI
PlatformEmailConfig(SMTP / SendGrid creds)Mail relay creds
AIConfig(API keys)Provider keys

Audit & encryption

RLS-protected tables

Tenant isolation via Postgres Row-Level Security policies on org-scoped tables. The OrganizationContextMiddleware sets app.current_org_id per request via SET LOCAL inside a transaction.atomic(). Policies filter rows based on this session variable.

Tables with RLS as of v0.57:

  • All tables in retention, compliance, elections, ai_services (reports + embeddings)
  • More are being migrated as part of an ongoing initiative

Multi-tenancy & RLS

Counts (current)

DomainModels
Organizations + memberships9
Members + profiles8
Compliance9
Finances10
Learning12
Forums5
Messaging7
Foundation7
Elections6
AI services8
Notifications4
Common (audit, etc.)2
Other (15 apps)70
Total~157

Naming conventions

  • Primary keys: id (UUID)
  • Foreign keys: <related_model_name>_id
  • Timestamps: created_at, updated_at (via apps.common.models.TimeStampedModel)
  • Soft-delete: is_active boolean (no SQL DELETE — preserves history)
  • Status enums: defined as TextChoices on each model
  • JSON fields: metadata for arbitrary extension data; specific fields when shape is known

Migrations

  • All migrations live in backend/apps/<app>/migrations/
  • Run with python manage.py migrate
  • Always reversible — no RunPython.noop for forward-only logic
  • Data migrations preferred over fixture loads for non-test data
  • Schema changes in production go through PR review + are applied via migrate in a Kubernetes Job before pod rollout

Where to find more detail