Schema
Complete reference for the database schemas.
The database is organized into three schemas:
cases- Court opinions and caselaw data (current)opinions- The previous shape of the same data. Deprecated — see Migrating fromopinionstocaseslaws- Statutes and regulations
casesis where new work should go. It carries everythingopinionscarried, plus the full docket rather than opinions alone: roughly 17 million additional docket entries and their document metadata.opinionscontinues to work unchanged during the overlap window and will be withdrawn on a date we will announce with notice.
Cases Schema
The cases schema is the current shape of our caselaw data. Where opinions
carried opinions only, cases carries the whole docket of every case it
publishes — motions, orders, notices and the rest — along with document
metadata for each entry.
Two structural differences are worth reading before you write queries:
-
An opinion and a docket entry are now separate rows.
opinions.docket_entrieswas one wide table; it is nowcases.docket_entriesjoined tocases.opinionsondocket_entries.opinion_id = opinions.id. An entry with aNULLopinion_idis a non-opinion docket entry — the new corpus. -
Text is keyed by document, not by opinion.
opinions.contentbecamecases.documents⋈cases.documents_content, joined ondocuments_content.id = documents.id.
Every table carries is_promoted, which is always true in your replica — it
is the flag that determines what reaches you at all. You can ignore it; it is
present because our replication requires it.
Every table also carries deleted_at. Rows are not removed when deleted;
they are marked. Filter WHERE deleted_at IS NULL for live rows. See
Change Tracking for the one case this does
not cover.
Entity Relationship
courts
└── cases
└── docket_entries ──── opinions (entries with an opinion_id)
└── documents
└── documents_content
opinions
└── citations
document_relations (citation graph)
└── treatments (how the citing document treats the cited one)
Cases Tables
cases.courts
Court definitions for federal, state, territorial, tribal, military, and international jurisdictions. Use in_use to distinguish courts that are actively covered.
| Column | Type | Description |
|---|---|---|
id 🔑 |
VARCHAR(50) | Court identifier |
pacer_court_id |
VARCHAR(10) | This court's PACER identifier, where it has one |
full_name |
TEXT | Full court name |
court_abbreviation |
VARCHAR(100) | Citation-form abbreviation |
jurisdiction |
VARCHAR(50) | Jurisdiction |
state |
VARCHAR(50) | State |
country |
VARCHAR(2) | ISO country code |
freelaw_court_id |
VARCHAR(50) | CourtListener / Free Law Project identifier |
in_use |
BOOLEAN | Whether this court is actively covered |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
pacer_court_id and freelaw_court_id are the join keys if you also license PACER/RECAP or CourtListener data.
cases.cases
One row per case.
| Column | Type | Description |
|---|---|---|
id 🔑 |
TEXT | Case identifier |
court_id → |
VARCHAR(50) | The court |
docket_number |
TEXT | Docket number |
case_name |
TEXT | Case name |
date_filed |
DATE | Date filed |
date_terminated |
DATE | Date terminated, where known |
source_id |
VARCHAR(50) | The source's own public identifier for this case (PACER case id, where applicable) |
data_origin |
VARCHAR(25) | Which acquisition path produced this case |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
cases.docket_entries
One row per document filed in a case. This is the table that grew: it now includes non-opinion filings.
| Column | Type | Description |
|---|---|---|
case_id 🔑 → |
TEXT | The case |
id 🔑 |
TEXT | Entry identifier |
opinion_id → |
TEXT | The opinion this entry is, or NULL if it is not an opinion |
source_id |
VARCHAR(50) | The source's own identifier for this entry |
date_filed |
DATE | Date filed |
entry_number |
BIGINT | Docket entry number |
description |
TEXT | Docket text |
data_origin |
VARCHAR(25) | Which acquisition path produced this entry |
doc_source |
VARCHAR(50) | Where the document came from |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
Two things to know:
opinion_id IS NOT NULLis how you identify an opinion. This replaces the oldis_opinionboolean.opinion_idis not unique. One opinion can be reached from several entries in several cases. If your code assumes one row per opinion, it needs aDISTINCTor a join throughcases.opinions.
The primary key is (case_id, id).
cases.opinions
The opinion-specific attributes of a docket entry.
| Column | Type | Description |
|---|---|---|
id 🔑 |
TEXT | Opinion identifier. Stable across the migration — the same value the old opinions.docket_entries.id carried |
judge_names |
TEXT[] | Authoring judges. An array, where the old schema had a single text field |
opinion_role |
ENUM | majority, concurrence, dissent, combined |
publish_status |
ENUM | published, unpublished, in_chambers, separate, errata, relating_to, unknown |
per_curiam |
ENUM | yes, no, unknown |
en_banc |
ENUM | yes, no, unknown |
precedential_note |
TEXT | Precedential status note |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
cases.documents
Document metadata for a docket entry.
| Column | Type | Description |
|---|---|---|
id 🔑 |
TEXT | Document identifier |
case_id → |
TEXT | The case |
docket_entry_id → |
TEXT | The docket entry |
document_type |
ENUM | opinion, brief, motion, order, transcript, judgment, complaint, and others |
file_type |
VARCHAR(100) | File type |
title |
TEXT | Document title |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
cases.documents_content
Full text, keyed by document.
| Column | Type | Description |
|---|---|---|
id 🔑 → |
TEXT | Document ID (same as documents.id) |
html_content |
TEXT | Document text in HTML format |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
cases.citations
Citations extracted from opinions.
| Column | Type | Description |
|---|---|---|
id 🔑 |
UUID | Citation identifier |
opinion_id → |
TEXT | The opinion this citation appears in |
cited_as |
TEXT | The citation as presented |
reporter |
VARCHAR(50) | Reporter |
volume |
VARCHAR(20) | Volume |
page |
VARCHAR(20) | Page |
pincite |
INTEGER | Pinpoint page |
year |
INTEGER | Year |
citation_type |
VARCHAR(50) | Citation type |
normalized |
VARCHAR(150) | Normalized form, for matching |
court_id → |
VARCHAR(50) | The court |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
cases.document_relations
The citation graph: one row per citing→cited edge. Where the old schema put the edge and its treatment in one table, the edge lives here and the treatment in cases.treatments.
| Column | Type | Description |
|---|---|---|
id 🔑 |
UUID | Edge identifier |
citing_table |
ENUM | What kind of thing is citing (opinion today) |
citing_id |
TEXT | The document doing the citing |
cited_table |
ENUM | What kind of thing is cited (opinion today) |
cited_id |
TEXT | The document being cited |
is_authoritative |
BOOLEAN | Whether the citation is authoritative. NULL means not yet evaluated, not "evaluated and found non-authoritative" |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
citing_table and cited_table are opinion for every edge today. They exist so that citations to statutes and regulations can be added later without changing the shape of this table.
cases.treatments
How a citing document treats the document it cites. Joins to document_relations on relation_id = document_relations.id.
| Column | Type | Description |
|---|---|---|
relation_id 🔑 → |
UUID | The edge in document_relations |
treatment_category |
TEXT | How the citing document treats the cited one |
treatment_description |
TEXT | Detailed treatment explanation |
supporting_quote |
TEXT | Quote from the citing document supporting the treatment |
is_promoted |
BOOLEAN | Always true |
deleted_at |
TIMESTAMPTZ | Soft-delete marker |
created_at |
TIMESTAMPTZ | When this row first reached your replica |
updated_at |
TIMESTAMPTZ | When this row last changed in your replica |
🔑 = Primary Key, → = Foreign Key
Opinions Schema
Deprecated. Superseded by the
casesschema. Still live and still maintained during the overlap window; no date is set yet. The mapping between the two is in Migrating fromopinionstocases.
The opinions schema contains normalized court opinion data. All tables are in the opinions schema and can be joined using foreign key relationships.
Entity Relationship
courts
└── cases
└── docket_entries
├── citations
└── content
opinion_relations (cross-references between opinions)
Opinions Tables
opinions.courts
Court definitions for federal, state, territorial, tribal, military, and international jurisdictions. Use in_use to distinguish courts that are actively covered.
| Column | Type | Description |
|---|---|---|
id 🔑 |
VARCHAR(50) | Court identifier (e.g., 'ca9', 'nysd', 'scc') |
full_name |
TEXT | Full court name |
court_abbreviation |
VARCHAR(100) | Citation abbreviation |
jurisdiction |
VARCHAR(50) | Federal Appellate, Federal District, Federal Bankruptcy, Federal Special, State Supreme, State Appellate, State Trial, State Special, State Attorney General, Military, Military Appellate, Tribal, U.S. Territory, Committee, International |
state |
VARCHAR(50) | State/province name, 'Federal', 'Tribal', or territory name |
country |
VARCHAR(2) | ISO 3166-1 alpha-2 country code (US, CA, etc.) |
in_use |
BOOLEAN | Whether court is actively scraped |
opinions.cases
Case metadata including docket information and dates.
| Column | Type | Description |
|---|---|---|
id 🔑 |
TEXT | Internal case ID |
court_id → |
VARCHAR(50) | Court identifier |
docket_number |
TEXT | Court docket number |
case_name |
TEXT | Case name/caption |
date_filed |
DATE | Date case was filed |
date_terminated |
DATE | Date case was terminated |
data_origin |
VARCHAR(25) | Source of the data |
opinions.docket_entries
Opinion entries (where is_opinion = true). Each row represents a court opinion.
| Column | Type | Description |
|---|---|---|
id 🔑 |
TEXT | Opinion identifier |
case_id → |
TEXT | Parent case |
description |
TEXT | Opinion description |
date_filed |
DATE | Opinion filing date |
judge_name |
TEXT | Authoring judge name |
is_opinion |
BOOLEAN | Whether this is an opinion (true for opinions) |
opinion_role |
ENUM | Role of opinion (lead, concurrence, dissent, etc.) |
publish_status |
ENUM | Publication status |
data_origin |
VARCHAR(25) | Source of the data |
opinions.citations
Reporter citations for opinions.
| Column | Type | Description |
|---|---|---|
id 🔑 |
UUID | Internal citation ID |
opinion_id → |
TEXT | Parent opinion |
court_id → |
VARCHAR(255) | Court identifier |
volume |
VARCHAR(20) | Reporter volume number |
reporter |
VARCHAR(50) | Reporter abbreviation (e.g., 'U.S.', 'F.3d') |
page |
VARCHAR(20) | Starting page number |
cited_as |
TEXT | Full citation string |
citation_type |
VARCHAR(50) | Type of citation |
year |
INTEGER | Year of citation |
normalized |
VARCHAR(150) | Normalized citation format |
opinions.content
Full text content for opinions.
| Column | Type | Description |
|---|---|---|
id 🔑 |
TEXT | Opinion ID (same as docket_entries.id) |
html_content |
TEXT | Opinion text in HTML format |
created_at |
TIMESTAMP | When content was added |
opinions.opinion_relations
Citation relationships and treatment analysis between opinions.
| Column | Type | Description |
|---|---|---|
cited_id → |
TEXT | The opinion being cited |
citing_id → |
TEXT | The opinion doing the citing |
citator_version |
TEXT | Version of citator analysis |
is_authoritative |
BOOLEAN | Whether this is an authoritative citation |
treatment_category |
TEXT | How the citing opinion treats the cited opinion |
treatment_description |
TEXT | Detailed treatment explanation |
supporting_quote |
TEXT | Quote from citing opinion supporting the treatment |
🔑 = Primary Key, → = Foreign Key
Migrating from opinions to cases
Everything opinions gave you is available in cases. Most of the work is
mechanical; three changes need a real edit.
opinions |
cases |
Note |
|---|---|---|
opinions.docket_entries (opinions only) |
cases.opinions ⋈ cases.docket_entries |
One table becomes two. Join on docket_entries.opinion_id = opinions.id |
docket_entries.is_opinion |
docket_entries.opinion_id IS NOT NULL |
Derived rather than stored |
docket_entries.judge_name (TEXT) |
opinions.judge_names (TEXT[]) |
Type change |
opinions.content |
cases.documents ⋈ cases.documents_content |
Text is keyed by document id now, not opinion id |
opinions.opinion_relations |
cases.document_relations ⋈ cases.treatments |
One table becomes two. Authority is on the edge, treatment text on the child |
opinion_relations.citator_version |
— | Removed; it described our internal analysis run |
| — | every non-opinion docket entry, plus document metadata | New: roughly 17 million additional entries |
| — | per_curiam, en_banc, precedential_note, title, pincite, citation_type, date_terminated, country, in_use, pacer_court_id, freelaw_court_id |
New columns |
Opinion IDs are unchanged. cases.opinions.id holds exactly the value
opinions.docket_entries.id held, so any mapping you keep keyed on opinion id
survives the move untouched. Document, citation and relation IDs are new.
The three real edits
1. is_opinion → opinion_id IS NOT NULL.
-- before
SELECT * FROM opinions.docket_entries WHERE is_opinion = true;
-- after
SELECT * FROM cases.docket_entries WHERE opinion_id IS NOT NULL;
2. Opinion text now joins through documents.
-- before
SELECT de.id, c.html_content
FROM opinions.docket_entries de
JOIN opinions.content c ON c.id = de.id
WHERE de.is_opinion = true;
-- after
SELECT de.opinion_id, dc.html_content
FROM cases.docket_entries de
JOIN cases.documents d ON d.docket_entry_id = de.id
JOIN cases.documents_content dc ON dc.id = d.id
WHERE de.opinion_id IS NOT NULL
AND de.deleted_at IS NULL;
3. Treatments join through the edge.
-- before
SELECT citing_id, cited_id, treatment_category, is_authoritative
FROM opinions.opinion_relations;
-- after
SELECT r.citing_id, r.cited_id, t.treatment_category, r.is_authoritative
FROM cases.document_relations r
LEFT JOIN cases.treatments t ON t.relation_id = r.id;
Use LEFT JOIN: an edge can exist without a treatment.
Two things that are not carried over
Citation counts. The old surface exposed a precomputed citation_counts
view. It is not part of cases; compute it directly:
SELECT cited_id, count(*) AS citation_count
FROM cases.document_relations
WHERE deleted_at IS NULL
GROUP BY cited_id;
If that aggregate is on a hot path for you, tell us — we would rather know than have you discover the cost in production.
_active views. There are none, in either schema. Filter
WHERE deleted_at IS NULL yourself, as you do today.
Laws Schema
The laws schema contains statutes (codified laws), regulations (administrative rules), and constitutions. This schema uses a unified document structure with hierarchical support and versioning.
Entity Relationship
collections (uscode, cfr, state codes)
└── documents (hierarchical: title > chapter > section)
└── version_relations (lifecycle edges between document versions)
Laws Tables
laws.collections
Content collections (e.g., U.S. Code, CFR, state statutes and regulations).
| Column | Type | Description |
|---|---|---|
id 🔑 |
VARCHAR(50) | Collection identifier (e.g., 'uscode', 'cfr') |
name |
VARCHAR(200) | Full name (e.g., 'United States Code') |
type |
VARCHAR(30) | Content type: statute, regulation, constitution, guidance, executive_order, or notice |
authority_name |
VARCHAR(200) | Publishing authority (e.g., 'United States', 'California') |
state |
VARCHAR(50) | Jurisdiction name (e.g., 'Federal', 'California', 'New York') |
country |
VARCHAR(2) | ISO 3166-1 alpha-2 country code |
created_at, updated_at |
TIMESTAMPTZ | Record timestamps |
deleted_at |
TIMESTAMPTZ | Soft-deletion timestamp |
Use GET /api/v1/laws/collections for the current machine-readable list, or
the live coverage overview for a human-readable view.
laws.documents
Unified table for hierarchical legal content (statutes, regulations, constitutions).
| Column | Type | Description |
|---|---|---|
id 🔑 |
UUID | Document identifier |
collection_id → |
VARCHAR(50) | Parent collection (e.g., 'uscode', 'cfr') |
parent_id → |
UUID | Parent document node |
path |
LTREE | Hierarchical path for tree queries (e.g., 'uscode.t5.ch1.s101') |
sort_order |
INTEGER | Order within parent |
node_type |
VARCHAR(20) | Structural type: PARENT, LEAF, or TOMBSTONE |
number |
VARCHAR(50) | Section/chapter number (e.g., '5', 'I', '101', '(a)') |
heading |
TEXT | Section heading/title |
citation_canonical |
VARCHAR(200) | Canonical citation (e.g., '5 U.S.C. § 552', '26 C.F.R. § 1.61-1') |
effective_date |
DATE | When this version became effective |
publication_date |
DATE | Source-stated publication date |
source_as_of |
DATE | The source's own as-of or edition date |
is_historical |
HISTORICAL_STATUS | YES when the source marks this version superseded; otherwise SCRAPER_DIDNT_SAY |
html_content |
TEXT | HTML formatted content |
content_hash |
VARCHAR(64) | Hash used to detect content changes |
source_law |
VARCHAR(500) | Public law reference for statutes (e.g., 'Pub. L. 118-100') |
agency_name |
VARCHAR(200) | Agency name for regulations (e.g., 'Environmental Protection Agency') |
agency_code |
VARCHAR(100) | Agency code for regulations (e.g., 'EPA') |
agencies |
JSONB | Structured agency metadata, when supplied by the source |
register_citation |
TEXT | Federal or state register citation (e.g., '89 FR 12345') |
source_id |
VARCHAR(200) | External source identifier |
source_url |
TEXT | Official source URL |
gcs_path |
VARCHAR(500) | GCS path to original source file |
first_scraped_at, last_scraped_at |
TIMESTAMPTZ | Observation timestamps |
created_at, updated_at |
TIMESTAMPTZ | Record timestamps |
deleted_at |
TIMESTAMPTZ | Soft-deletion timestamp |
Currency is derived at query time from the document and its lifecycle edges;
there is no stored is_current, status, end_date, or supersedes_id column.
laws.version_relations
Lifecycle and lineage edges between document versions. Edges point from the
newer or terminal-marker row (from_id) to its predecessor (to_id). Dates
remain on the document rows rather than the edge.
| Column | Type | Description |
|---|---|---|
id 🔑 |
UUID | Relation identifier |
from_id → |
UUID | Newer version or lifecycle marker |
to_id → |
UUID | Predecessor version |
relation_type |
VARCHAR(30) | succeeds, transferred, removed, or repeals |
created_at |
TIMESTAMPTZ | Edge creation timestamp |
🔑 = Primary Key, → = Foreign Key