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 from opinions to cases
  • laws - Statutes and regulations

cases is where new work should go. It carries everything opinions carried, plus the full docket rather than opinions alone: roughly 17 million additional docket entries and their document metadata. opinions continues 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:

  1. An opinion and a docket entry are now separate rows. opinions.docket_entries was one wide table; it is now cases.docket_entries joined to cases.opinions on docket_entries.opinion_id = opinions.id. An entry with a NULL opinion_id is a non-opinion docket entry — the new corpus.
  2. Text is keyed by document, not by opinion. opinions.content became cases.documents ⋈ cases.documents_content, joined on documents_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 NULL is how you identify an opinion. This replaces the old is_opinion boolean.
  • opinion_id is not unique. One opinion can be reached from several entries in several cases. If your code assumes one row per opinion, it needs a DISTINCT or a join through cases.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 cases schema. Still live and still maintained during the overlap window; no date is set yet. The mapping between the two is in Migrating from opinions to cases.

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