Database Architecture
Comprehensive overview of the Trackr database architecture, schema, indexes, relationships, and Row Level Security policies.
Database Architecture
The Trackr application relies on a highly optimized, robust PostgreSQL database to manage users, job applications, companies, interviews, and real-time analytics. This document provides a complete guide to our database systems, including schema definitions, indexes, relationships, Row Level Security (RLS) policies, and our migration strategy. Our database is designed to handle millions of job applications concurrently, providing low-latency queries and strict data isolation across tenants.
Overview
We use PostgreSQL 16+ as our primary relational database. It is managed via Prisma ORM for type-safe schema definitions and query building in our application layer, while utilizing native PostgreSQL features for advanced functionality like Full Text Search and JSONB document storage.
Our database architecture follows a multi-tenant design where all tenants (users/organizations) share the same database and schema, but their data is strictly isolated using PostgreSQL Row Level Security (RLS). This approach allows for efficient connection pooling, simplified schema migrations, and optimized resource utilization.
Core Schema & Tables
The Trackr database is normalized to the Third Normal Form (3NF) to reduce redundancy, with specific denormalizations introduced only where required for critical read-path performance.
users
The central table for authentication, authorization, and profile management.
id(UUID, Primary Key): Unique identifier for the user.email(VARCHAR(255), Unique): The user's email address.password_hash(VARCHAR(255)): Argon2 hashed password.first_name(VARCHAR(100)): User's first name.last_name(VARCHAR(100)): User's last name.role(ENUM('user', 'admin', 'moderator')): Role-based access control. Default is 'user'.created_at(TIMESTAMPTZ): Timestamp of account creation.updated_at(TIMESTAMPTZ): Timestamp of the last update.settings(JSONB): User-specific preferences (e.g., dark mode, notification settings).
Relationships:
- 1:N with
job_applications - 1:N with
resumes - 1:N with
user_sessions
companies
Stores information about the companies that users are applying to. This is a global table, but users can also create custom company entries that are private to them.
id(UUID, Primary Key)name(VARCHAR(255)): Name of the company.domain(VARCHAR(255)): Website domain, used for logo fetching and deduplication.industry(VARCHAR(100)): E.g., 'Software', 'Finance'.created_by(UUID, Foreign Key): Links tousers.idif the company is user-created. NULL if global.created_at(TIMESTAMPTZ)
Indexes:
idx_companies_nameonname(B-Tree) for autocomplete lookups.idx_companies_domainondomain(B-Tree) for uniqueness checks.
job_applications
The core transactional table storing the lifecycle of a job application.
id(UUID, Primary Key)user_id(UUID, Foreign Key): The owner of the application.company_id(UUID, Foreign Key): The company applied to.role_title(VARCHAR(255)): The title of the job.status(ENUM('bookmarked', 'applied', 'interviewing', 'offered', 'rejected', 'withdrawn')): Current state.applied_date(DATE): When the user applied.salary_range(INT4RANGE): PostgreSQL range type for expected salary.location(VARCHAR(255)): Job location.is_remote(BOOLEAN): Whether the role is remote.notes(TEXT): Free-form markdown notes.created_at(TIMESTAMPTZ)updated_at(TIMESTAMPTZ)
Relationships:
- N:1 with
users - N:1 with
companies - 1:N with
interviews - 1:N with
application_events
interviews
Tracks individual interview rounds associated with a job application.
id(UUID, Primary Key)application_id(UUID, Foreign Key): Link tojob_applications.id.round_name(VARCHAR(100)): E.g., 'Phone Screen', 'Onsite', 'Technical'.scheduled_at(TIMESTAMPTZ): When the interview takes place.duration_minutes(INTEGER): Expected length.format(ENUM('video', 'phone', 'in_person'))feedback(TEXT): Notes post-interview.
resumes
Stores references to uploaded resumes (stored in object storage like AWS S3).
id(UUID, Primary Key)user_id(UUID, Foreign Key)file_name(VARCHAR(255))s3_key(VARCHAR(512)): The path in the S3 bucket.parsed_data(JSONB): Extracted text and entities using our AI parser.
Indexing Strategy
To ensure sub-millisecond response times for common queries, we employ a rigorous indexing strategy.
- Primary and Foreign Keys: All primary keys (
id) and foreign keys automatically receive B-Tree indexes. This ensures fast JOINs and lookups. - Status and Dates: The
job_applicationstable has a composite index on(user_id, status, applied_date)to optimize the main dashboard view, which typically filters by status and sorts by date.CREATE INDEX idx_job_apps_dashboard ON job_applications (user_id, status, applied_date DESC); - JSONB Indexing: We use GIN (Generalized Inverted Index) on the
resumes.parsed_dataandusers.settingscolumns to allow fast querying of nested JSON attributes.CREATE INDEX idx_resumes_parsed_data ON resumes USING GIN (parsed_data); - Full Text Search: The
notescolumn injob_applicationsandfeedbackininterviewsare indexed using PostgreSQL'stsvectorfor fast keyword searches.CREATE INDEX idx_job_apps_notes_search ON job_applications USING GIN (to_tsvector('english', notes));
Row Level Security (RLS)
Trackr strictly enforces multi-tenancy at the database level using PostgreSQL Row Level Security. This ensures that a bug in the application layer cannot accidentally expose one user's data to another.
RLS is enabled on all tables that contain user-specific data.
Example: Job Applications RLS Policy
First, we enable RLS on the table:
ALTER TABLE job_applications ENABLE ROW LEVEL SECURITY;We then create policies for SELECT, INSERT, UPDATE, and DELETE:
-- Allow users to read their own applications
CREATE POLICY select_own_applications ON job_applications
FOR SELECT
USING (auth.uid() = user_id);
-- Allow users to insert applications for themselves
CREATE POLICY insert_own_applications ON job_applications
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Allow users to update their own applications
CREATE POLICY update_own_applications ON job_applications
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Allow users to delete their own applications
CREATE POLICY delete_own_applications ON job_applications
FOR DELETE
USING (auth.uid() = user_id);Note: The auth.uid() function is a custom PostgreSQL function that extracts the authenticated user's ID from a securely set session variable configured by our middleware before every transaction.
Migrations Strategy
Database schemas evolve. At Trackr, we treat database migrations as first-class citizens in our deployment pipeline. We use Prisma Migrate combined with custom raw SQL scripts for advanced PostgreSQL features.
Workflow
- Local Development: Developers modify the
schema.prismafile. Runningprisma migrate devgenerates a new SQL migration file. - Review: The generated
.sqlfile is committed to version control and reviewed. DBAs check for locking operations, index creation overhead, and backward compatibility. - Zero-Downtime Migrations: All migrations must be non-blocking.
- Instead of renaming columns directly, we add the new column, dual-write to both, backfill the new column, and finally drop the old column in a subsequent release.
- We create indexes
CONCURRENTLYto prevent table locking during deployment. Note that Prisma doesn't support concurrent index creation natively, so these must be written in raw SQL.
Example Migration: Adding a column concurrently
-- Step 1: Add column (fast operation, minimal lock)
ALTER TABLE job_applications ADD COLUMN expected_equity DECIMAL(5,2);
-- Step 2: Create index concurrently (does not block writes)
-- Note: This cannot run inside a transaction block
CREATE INDEX CONCURRENTLY idx_job_apps_equity ON job_applications (expected_equity);Data Retention and Archiving
To maintain high performance as the database grows, we implement partition-based archiving.
The application_events table (which logs every state change and interaction for analytics) grows rapidly. We use PostgreSQL native declarative partitioning to partition this table by month. Data older than 24 months is moved to cold storage (AWS S3) and dropped from the active PostgreSQL cluster, keeping index sizes manageable.
High Availability and Backups
- Replication: We run a multi-AZ setup with one primary writer and two read replicas. Read-heavy analytics dashboards are routed to the read replicas to offload the primary database.
- Backups:
- Automated continuous archiving (WAL shipping) to S3 allows Point-in-Time-Recovery (PITR) with up to 1-second granularity for the past 30 days.
- Daily full snapshots.
- Connection Pooling: We use PgBouncer configured in transaction mode sitting between the application servers and the database to handle connection spikes and reduce connection overhead.
By adhering to these architectural patterns, the Trackr database remains a secure, scalable, and highly performant foundation for the entire application ecosystem.