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:
| Status | Platform 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):
| Model | Field | Why |
|---|---|---|
User | phone_number | PII |
SAMLConfiguration | x509_cert | Identity provider secret |
OAuthConfiguration | client_secret | Identity provider secret |
LinkedInAccount | access_token, refresh_token | Third-party tokens |
BackupDestination | secret_access_key | Cloud secret |
OrgPaymentProcessor | (provider creds) | PCI |
PlatformEmailConfig | (SMTP / SendGrid creds) | Mail relay creds |
AIConfig | (API keys) | Provider keys |
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
Counts (current)
| Domain | Models |
|---|---|
| Organizations + memberships | 9 |
| Members + profiles | 8 |
| Compliance | 9 |
| Finances | 10 |
| Learning | 12 |
| Forums | 5 |
| Messaging | 7 |
| Foundation | 7 |
| Elections | 6 |
| AI services | 8 |
| Notifications | 4 |
| 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(viaapps.common.models.TimeStampedModel) - Soft-delete:
is_activeboolean (no SQLDELETE— preserves history) - Status enums: defined as TextChoices on each model
- JSON fields:
metadatafor 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.noopfor forward-only logic - Data migrations preferred over fixture loads for non-test data
- Schema changes in production go through PR review + are applied via
migratein a Kubernetes Job before pod rollout
Where to find more detail
- Per-app pages — full model lists per Django app
- API reference — every endpoint with request / response schemas
- Multi-tenancy & RLS — tenant isolation in detail