Skip to content

Entity graphs on managed Postgres

The working example serves its read surface through an edge cache, so it keeps answering from stale responses even while the disposable database behind it is destroyed - x-pggraph-cache on every response says which. The measurements below remain the reference for the design, and two corpora now sit under them: seven public-domain US federal documents (1521 citation entities, 10016 mentions, 14233 edges) and 103 Australian local-government public documents - one council’s minutes, committee papers, contracts registers and tender notices across 2015-2026 - which added the people, organisations and registry identifiers the editorial questions actually ask about (4787 entities, 28778 edges total). No graph database anywhere in it.

That line is a claim about capability, not category, so here is the boundary. The requirement this answers is a set of graph questions - who is connected to whom, by how strong a chain, clustered how, traceable to which source - not a graph product. A dedicated graph store is the right tool when the requirement is one of these instead: Cypher-grade pattern matching as the team’s primary query language, unbounded or deep variable-length traversal under concurrent load at much larger scale, or an existing investment in a graph-native ecosystem. This workload is none of those: read-heavy, batch-loaded, bounded-hop and provenance-heavy. If it eventually becomes one of those, the relational work below is not wasted - the extraction, resolution and provenance layers are the parts every pipeline keeps regardless of which store sits at the end.

To build it, follow the implementation guide

  • same system, ordered steps, one verification per part.

Everything below was measured on disposable projects: Postgres 17.6, medium compute (2 vCPU / 4 GB), in ap-southeast-1 between 2026-08-10 and 2026-08-11, and in ap-southeast-2 on 2026-08-14 for the editorial corpus run. Query latencies are EXPLAIN ANALYZE’s own Execution Time with the cold first run discarded, so they are database work rather than round trip. Read the compute as a floor - a real deployment provisions more. Claims that are documented rather than measured are marked in Evidence.

TL;DR:

  • No property-graph store. Apache AGE is absent from the 78 available extensions, and there is no Cypher.1 Say so plainly.
  • pgrouting 3.4.1 is available and is not geospatial-only: 209 pgr_* functions, including Dijkstra, breadth- and depth-first search, k-shortest paths, connected components, articulation points, bridges and transitive closure, over ordinary relational edge tables.2
  • Recursive CTE traversal mostly settles the question. Depth-3 neighbourhood over 100000 entities / 400000 edges runs in about a fifth of a millisecond with two indexes present, against roughly 167.59 milliseconds without them, on a 602 millisecond index build,3 and holds flat to 64 concurrent readers (p50 ~0.3 ms, zero errors).
  • Two name collisions derail this discussion. pg_graphql is a GraphQL API over tables.4 ltree models single-parent hierarchies, and an entity graph is not a tree.5
  • SQL/PGQ is committed for PG19 and does not change the engine - it is rewritten to a standard relational query in the rewriter.6
  • Extraction belongs outside the database, and which runtime you pick moves the ceiling by 5.6x.
  • Do not size a corpus from its PDF bytes. The extracted-text ratio measured 0.048 to 1.0885 across seven documents.
  • Measured on a fixed corpus. The evidence below separates measured behaviour from documented claims. The person and organization extractor is a candidate-generation example, not a production named-entity recognizer: a 24-row review of 561 persons and 2284 organizations found truncated names, labels split at line breaks and occasional sentence-level over-captures.
  • Cross-document bridging is the payoff, and the corpus has to be built for it. 20 of the 1521 US citation entities span documents; the council corpus engineered for overlap puts sitting councillors in 86-93 of its 103 documents.
  • Registry identifiers make resolution arithmetic, not similarity. Every ABN candidate shape in the corpus (148 of them) was checked against the mod-89 checksum at extraction: 14 valid mentions, 134 rejected, zero false positives surviving.
  • Real co-occurrence graphs have hubs, and hubs break naive walks. A path-set recursive walk (UNION ALL) took 65721.220 ms at depth 3 on the 28778-edge real graph; level-set semantics (UNION) answers the same query in 154.924 ms. The synthetic benchmark never showed it: its average degree is 8.

Outside the databasePostgresSource documentsParse / OCREntity + relationextractiondocumentstext + provenanceEntity resolutionentitiesedgesid, source, target, costPostgRESTrecursive CTEneighbourhoodpgroutingpaths, components,bridgespg_trgm +fuzzystrmatchresolutionStatic UIon Workers

The three stages on the left are yours. That split is not a limitation of Postgres so much as how document-to-graph pipelines are built generally: the graph store is the last and most swappable stage, and the hard parts are extraction, entity resolution and provenance.


QuestionUseMeasured
Who is within N hopsRecursive CTE written as a level set (UNION), indexes on both edge columns0.22 ms at depth 3 on the synthetic graph (average degree 8); 154.924 ms on the real 28778-edge graph, and 65721.220 ms if the walk is a path set (UNION ALL)
What connects these two, how stronglypgr_dijkstra, cost as inverse evidence weight1092.61 ms over 400000 edges
Does the data clusterpgr_connectedComponents1098.11 ms
What is structurally criticalpgr_articulationPoints, pgr_bridges9883.44 ms, 254 bridges
Which things are named similarlypg_trgm + fuzzystrmatchindex-backed
Which documents mention XSTORED tsvector generated column + GIN, queried with websearch_to_tsquery112.6 ms median inner query, GIN 3192 kB, on 7 rows
Which things mean something similarpgvector as halfvec with an HNSW index0.43 ms HNSW query against 46.13 ms IVFFlat (lists=100) over 30000 x 1536 synthetic vectors; recall unmeasured - see Agent memory store

The CTE expands a bounded neighbourhood; pgr_dijkstra computes a global shortest path across the whole edge set.

pgr_* functions require an inner query exposing columns named exactly id, source, target and cost, optionally reverse_cost. The first three are typed ANY-INTEGER and the costs ANY-NUMERICAL.7 Two consequences:

  • A uuid primary key cannot be handed to pgr_*. Carry a surrogate integer key on entities if you want the algorithms.
  • Naming your columns source_id and target_id, the natural relational choice, forces an aliasing subquery at every call site.

Setting cost to the inverse of an evidence weight makes the cheapest path the most strongly evidenced one rather than the shortest hop count.


78 extensions available. Read the graph answer as two facts, because either alone misleads:

CapabilityStatus
Property-graph store: node/edge storage plus a traversal languageNone available. No age, agensgraph, apache_age, sqlg
Graph-algorithm library: algorithms over relational edge tablespgrouting 3.4.1, requiring postgis

“No graph database” overstates the gap. “There is a graph extension” overstates the capability. Both sentences are needed.

Also present, none of them graph stores but several of them load-bearing below:

ExtensionVersionRole in this design
vector0.8.2semantic similarity; storage, build and query latency measured (G05), recall not
pg_trgm1.6name resolution, fuzzy fallback
fuzzystrmatch1.2name resolution, fuzzy fallback
unaccent1.1name normalisation
ltree1.3hierarchies only - not the answer here
rum1.3ranking indexes
pgroonga3.2.5full-text search
pgmq1.5.1ingestion queue
pg_cron1.6.4scheduling
pg_net0.20.4outbound HTTP from SQL
http1.6outbound HTTP from SQL
pg_partman5.3.1table partitioning
hypopg1.4.1hypothetical indexes
  • pg_graphql exposes a GraphQL API over relational tables.4 It shares nothing with a graph database but the word. Anyone grepping an extension list for graph finds it and reports the question closed.
  • ltree models labelled paths through single-parent hierarchies.5 A tree is not a graph; it fails on the first node with two parents.

Both get offered as the answer. Neither is, today.

Apache AGE is not installable on managed Postgres here. It is absent from the 78 available extensions, so CREATE EXTENSION age cannot succeed. Upstream is active - releases exist for PG17 and PG18, and a PG19 branch exists - but those are release candidates, and availability is decided by whether the provider packages it.8 That makes it a packaging request, not a design option.

SQL/PGQ is real, committed, and syntax rather than an engine. It was committed on 2026-03-16 into the PG19 cycle, implementing the SQL:2023 standard’s GRAPH_TABLE pattern matching plus CREATE PROPERTY GRAPH DDL.6 9 Three things follow:

  1. PG19 is unreleased. The newest supported major series is PG18,10 and the platform measured here runs 17.6 - so the wait is PG19 general availability and then provider adoption.
  2. The commit notes the security-definer variant is not implemented.
  3. A property graph is a new relkind that, in the commit’s own words, “acts like a view in many ways” and “is rewritten to a standard relational query in the rewriter.”

That third point decides it. PGQ replaces hand-rolled recursive CTEs with declarative graph patterns, which is a real improvement in how queries are written. The plan underneath is the relational plan measured above. Waiting buys ergonomics; the capability is already here.


PDF-to-text and OCR extensions exist. pgpdf adds a pdf type castable from bytea, parsing via poppler.11 pg_ocr runs an ONNX pipeline in-database.12 Neither is reachable on managed Postgres: neither is in the catalogue, and pg_ocr needs plpython3u while the only procedural languages available are plpgsql and plpgsql_check. Untrusted PL is arbitrary code execution as the database user, so its absence is a deliberate provider position.

It would not pay off anyway. Extracting all seven documents with pdftotext took about eleven seconds, a few percent of the pipeline, and pgpdf uses poppler too - so the best case is the same work on the database’s CPU instead of a stateless worker’s. Parsing is also the stage that most wants to scale horizontally, and the database is the component that cannot.

One boundary belongs in writing before anyone discovers it: scanned documents extract to nothing, silently. A corpus spanning decades includes scans, and a born-digital pipeline produces zero entities from them and looks broken. Detection is measured: an image-only scan yields 0.5 characters per page against 2710-3242 for born-digital references, so anything under roughly 270 characters per page routes to OCR. OCR itself is another external stage, not a database one.

Same task, same fixtures, same unpdf/pdf.js build, both V8 isolates - so this measures the runtime envelope, not the parser.

Fixture bytesSupabase Edge FunctionCloudflare Worker
191290okok - 40 pages
2504695ok, its ceilingok - 188 pages
3595043HTTP 546ok - 894 pages
6073678HTTP 546ok - 492 pages
14034445HTTP 546ok - 2780 pages
14930674HTTP 5461102 / HTTP 503

Cloudflare’s failure was disambiguated rather than left as “resource limit”: error 1102 covers CPU or memory indistinguishably from the HTTP response, and the analytics API reported exceededMemory. So it is the 128 MB per-isolate limit, not CPU - the CPU ceiling was raised to the documented paid-plan maximum.13

The Edge Function has a second bound the fixtures never reached: a 150 s idle wall clock, after which the invoke dies with 504 IDLE_TIMEOUT (edge-resilience W13, 2026-08-15). The two 14 MB fixtures failed with the 546 at 57165 ms and 60136 ms, well inside 150 s, so on this corpus the resource limit binds before the idle clock.

A static read UI can live anywhere, since every data call goes browser to PostgREST with an anon key. Supabase Storage is not one of those places: it records index.html as text/html and serves it as text/plain with x-content-type-options: nosniff, so a browser renders the source as text. JS and CSS serve with correct types, so the downgrade is HTML-specific and deliberate - a public bucket takes arbitrary uploads, and serving HTML as HTML would make it a stored-XSS surface on the project’s own origin.

The live demo is therefore a Cloudflare Workers static-assets deployment, which serves text/html correctly. The same worker also proxies the read RPCs through the documented Cache-API pattern for POSTs - body hashed into a synthetic GET cache key - which buys two properties the demo’s disposable database needs. Repeated reads (stats, the document inventory, the bridging list) answer at the edge in single-digit milliseconds rather than round-tripping to the project region. And when the project is destroyed between engagements, the demo keeps answering from stale cache entries instead of 502ing: freshness is tracked with an x-cached-at stamp against a six-hour TTL, and any origin failure falls back to whatever is stored, marked x-pggraph-cache: STALE. Two implementation notes cost real time to learn: the Cache API rejects put() of any response carrying Set-Cookie (Cloudflare’s own edge adds __cf_bm, so strip it on the stored copy or every request is a silent permanent MISS), and the store is per-colocation with an asynchronous write, so two back-to-back probes can both MISS without anything being broken.

Cloudflare Worker (pggraph)browserstatic assets binding(page, JS, CSS)GET pageworker script/rest/* onlyPOST /rest/v1/rpc/*Cache APIper-colo storesynthetic GET key(path + body hash + version)store (Set-Cookie stripped)PostgRESTMISSHIT in TTLSTALE when origin downPostgres(disposable demo project)

Driving ingestion: a queue in the database, workers outside it

Section titled “Driving ingestion: a queue in the database, workers outside it”

Once extraction lives outside the database, something has to hand work to it and collect the results. pgmq is available on the platform, alongside pg_cron, pg_net and http,1516 and the pattern of queue-in-Postgres plus an external consumer is already documented for embedding generation.1718

What makes it a good fit here:

  • The queue and the data are one database, so a consumer can delete the message and insert the extracted rows in a single transaction. A separate broker reintroduces the dual-write problem and needs idempotency keys to cover it.
  • Consumers are stateless, which is what parsing wants: it is the stage that parallelises, and the database is the component that cannot be scaled out by adding replicas.
  • Scale-to-zero consumers make a backfill cost nothing between runs.19
  • Region matters: put consumers in the region hosting the database, not wherever is convenient.20

The documented version of this pattern uses serverless functions as the consumer. For PDF work that substitution is wrong on the measurements above - the platform’s own function runtime stopped at 2504695 bytes. A long-running machine has neither that ceiling nor the 128 MB isolate limit.

Three more things to set deliberately:

  • Bulk writes should not go through the transaction pooler. It silently drops connection-time options, and large TOASTed writes are not what it is for. Use the direct connection for ingestion and keep the pooler for the read API.
  • Visibility timeout must exceed the real p99 job duration. Measure first. Too low duplicates work; too high means a crashed consumer holds its message that long.
  • A queue can be over-engineering. For a one-time historical backfill with a light ongoing tail, a bounded parallel runner is enough - fetching 40 MB across seven documents took about a minute at seven-way concurrency. Reach for the queue when arrival is continuous and unpredictable, or when per-document retry and audit are requirements.

Sizing a corpus you have not extracted yet

Section titled “Sizing a corpus you have not extracted yet”

The shortcut is to assume extracted text is a small fraction of the PDF and multiply. Measured across seven public-domain federal documents:

Genreextracted / sourceTOAST / logical
Positioned form0.0480.4765
Standard, control catalogue0.25730.3744
Budget narrative0.27750.4132
Legislation0.57990.3464
Annotated constitution0.84900.4668
Budget appendix0.90490.3387
Dense regulation1.08850.3896
Aggregate0.76330.3965
  • Extracted text can exceed its own PDF. Dense regulation came out above 1.0, because PDF already compresses its content streams.
  • Genre does not predict the ratio. The two regulation documents landed at 0.8490 and 1.0885; the two budget documents at 0.2775 and 0.9049. Text density and prior compression predict it.
  • TOAST roughly halves the stored size again, so logical bytes and database footprint are also different numbers.

A projection built on 5% is wrong by roughly 6x at the aggregate and by more than 20x on the least favourable document. Extract a genre-spanning sample and measure. Quote the result per document: a corpus projection stated per month of infrastructure cannot be compared against anything priced per document, and the comparison gets made in whatever units are quoted.

A worked figure from the council corpus (G12, inputs measured 2026-08-14; the per-GB rates are Supabase list prices dated 2026-08-102122): a minutes-like document ran about 430 KB of source PDF to about 11 KB of logical text and about 5.4 KB on disk after TOAST. At $0.125/GB/mo for database disk and $0.0213/GB/mo for the raw PDF in Storage, that is about a cent per thousand documents per month on the Supabase side, compute not priced. A 4 TB source corpus at that ratio projects to about 102 GB logical and about 50 GB on disk - roughly $6/mo of disk plus $85/mo of raw storage, about $92/mo; the same 4 TB at the US federal corpus’s 0.7633 aggregate ratio projected to about $239/mo. Genre moved the projection 2.6x between two measured corpora, which is why the per-document figure has to come from a sample of the reader’s own genres.


The citation layer proves the machinery; the editorial question is about people and organisations. Four properties of the loaded corpus matter more than its size.

20 of the 1521 US citation entities appear in more than one document - 1.3%, which is arithmetic rather than a flaw: seven documents across five genres share almost no citation surface. A corpus built to demonstrate bridging is one issuing body’s ordinary minutes, committee minutes, contracts registers and tender notices across consecutive years, where the same councillors and contractors recur by construction. Measured on the loaded graph: the top bridging entities are the sitting councillors at 86-93 of 103 documents each. The query side is cross_document_entities() - every entity spanning two or more documents, with the slugs attached.

Person and organization extraction is the noisy stage

Section titled “Person and organization extraction is the noisy stage”

The deterministic honorific and suffix patterns found 292 distinct persons and 123 organisations across the council documents (the 2026-08-14 run with the AU patterns; the 561 / 2284 counts in the TL;DR come from the 2026-08-11 US-pattern extractor over the earlier corpus and are recorded in this doc only, not in the RUNLOG). A graded 12+12 sample shows what to expect of them:

  • over-captured leading words (“Council accept the tender from Stabilicorp Pty Ltd”)
  • line-break splits and newline-glued names
  • honorific-only rows (“Mrs”)
  • job titles as people - “Director Corporate” spans 92 documents and is not one

Two organic lessons the US review could not teach: council resolutions are set in ALL CAPS, so the first Pty Ltd suffix pattern missed 3 of 103 documents until the suffix was made case-flexible, and a source-document typo (“Cr Jo Willians”) survives as its own entity - the fuzzy-resolution case arriving on its own. A production pipeline puts a model pass and a review step here; the demo’s job is the machinery, with the precision labelled rather than tuned toward plausible.

The time axis is a date on every mention and edge

Section titled “The time axis is a date on every mention and edge”

The editorial question is temporal - dealt with, five years ago, and in twenty other places. Minutes and filings are dated, so the metadata is free at ingestion: doc_date sits on every mention and edge, filled by a BEFORE INSERT trigger so no extractor needs to know. An entity’s history is entity_timeline() in document-date order, and every read has an as-at form: bridges_as_at() and neighbourhood_as_at() traverse only edges whose documents existed by the date. Measured discrimination on the recurring local builder: 0 neighbours as at 2020-01-01 against 171 as at 2026-12-31. The US documents carry no dates and are simply absent from as-at answers, which is the honest reading of “as at”: only what was known by then.

Registry identifiers make resolution arithmetic

Section titled “Registry identifiers make resolution arithmetic”

pg_trgm and fuzzystrmatch stay as the fallback. Australian documents print ABNs, and an ABN carries a checksum - subtract 1 from the first digit, weight by 10,1,3,5,7,9,11,13,15,17,19, and the sum must divide by 89. A checksum turns extraction into proof: 148 eleven-digit candidate shapes across the corpus, 14 valid mentions, 3 distinct entities, 134 rejected by arithmetic, zero false positives surviving. The join is exact where a key exists: the organisation WHITE ROCK WIND FARM PTY LTD co-occurs in a 2015 committee paper with its printed ABN, so a co-proximity edge pins the identity (entity_registry_ids() surfaces it), while the 2022 minutes name the same venture’s community fund through a councillor appointment. No fuzzy match in the pin; the trigram only links the name variants. Zero ACN tokens exist in this corpus, so no acn kind was built - an example of the corpus deciding the scope.

The traversal, bridging and cluster queries already existed, so the graph view is a radial node-link renderer over subgraph() output - concentric rings by CTE depth, no force layout, no dependency. Two discipline notes from doing it: hub nodes make full edge sets unreadable (the view draws the top 24 nodes by weight, top 3 edges per node, and says so), and the proof the graph is real is the table beside the picture, not the picture.


The practices the measurements support. Module ids are pdf-corpus-graph RUNLOG sections in supabase-lab unless named otherwise.

PracticeRests on
Write the recursive walk with UNION (level set), never UNION ALL, and cap the depth: a degree-917 hub took the path-set walk to about 65721 ms at depth 3 on the 28778-edge graph; the level-set walk answers in 155-159 ms warm over the poolerthe fusion run, 2026-08-14 (RUNLOG figures)
Carry a surrogate integer key and name the edge columns id, source, target, cost; set cost to the inverse evidence weightG07; the pgRouting contract7
Store embeddings as halfvec (0.5042 of vector bytes on identical columns) and index with HNSW (0.43 ms query against 46.13 ms IVFFlat at lists=100; build 6950 ms against 4130 ms); make no recall claim from synthetic vectorsG05
Provision disk throughput deliberately for a read-heavy corpus with vector indexes; it is a separate addon field (baseline_disk_io_mbs=347, max_disk_io_mbs=2085), not a function of disk sizeG06; Cost shape
Quote cost per document from measured per-genre ratios with a worked example, and label the per-GB rates as list prices with their date; genre moved the same 4 TB from about $92/mo to about $239/moG12
Set per-document fetch timeouts independent of byte size and cache fetched bytes: fetch was 9788 ms of an 11601 ms request at 2504695 bytes, and failures were not monotonic in size (6073678 bytes in 4105 ms, 3595043 bytes in 16906 ms, the 14 MB fixtures about a minute)G02
Give keyword search its own row: a STORED tsvector generated column with a GIN index, queried with websearch_to_tsquery so raw user input does not error (GIN 3192 kB; 112.6 ms median inner query; sequential scan chosen at 7 rows)search tier
When profiling, EXPLAIN ANALYZE the inner query or set pg_stat_statements.track = all; security-definer RPC bodies do not inline and are invisible under track = topsearch tier; sbperf audit
Use the direct connection or the session pooler for ingestion and set bulk-load timeouts in-session: through the transaction pooler show statement_timeout returns the 2min role default whether or not you set it, because connection-time options are droppedmethod notes
Treat the Edge Function as bounded by a resource limit (HTTP 546 WORKER_RESOURCE_LIMIT, CPU or memory not distinguished) somewhere between 2504695 and 3595043 bytes, and by a 150 s idle wall clock (504 IDLE_TIMEOUT); on this corpus the resource limit binds firstG02; edge-resilience W13
Route anything under about 270 characters per page to OCR outside the databaseG09

Consumer count, long-poll interval and visibility timeout in the ingestion section are stated as rules with no run behind them; the 400-character edge window and the renderer cut are editorial choices.


  • The index ratio generalises. Two indexes turning a 167.59 ms depth-3 traversal into 0.22 ms is a statement about access paths. Absolute values scale with graph size and compute.
  • The traversal latencies are a floor. Taken on 2 vCPU / 4 GB.
  • The expansion ratios are corpus-specific; the SPREAD is the point. Do not reuse 0.7633. Reuse the finding that one ratio is not safe.
  • The CTE traversal holds under concurrency. The depth-3 neighbourhood was pulled at concurrency 1, 4, 16 and 64 on the 100000/400000 graph: p50 flat at 0.3-0.7 ms, p95 at 2.9-3.6 ms, zero errors in 85 queries, on the same 2-vCPU medium - no knee, because the graph fits working memory. The pgrouting global algorithms are still single-query, uncontended numbers.
  • The two halves of the evidence met on 2026-08-14. Until then the traversal and pgrouting latencies came from a synthetic 100000/400000 graph and the extraction pipeline had run over seven real documents, so “it works” and “it is fast” were two claims resting on two artifacts. The run over roughly 100 real documents (111: 103 council documents, the seven US fixtures and the scan fixture) fused them and falsified the transfer - a degree-917 hub turned the path-set walk into about 65721 ms at depth 3 on the 28778-edge graph, and the level-set rewrite answers in 155-159 ms warm over the pooler (RUNLOG figures) - and produced the per-genre extraction ratios (0.0011 to 0.0362 across the council genres) that turn the cost projection from a band into a number (G12). The pgrouting latencies are still synthetic-graph only.
  • Extension availability is region-specific until probed. Everything here was measured in ap-southeast-1. Re-run the catalogue probe in the deployment-target region before quoting pgrouting there.

ClaimHow it was checked
78 available extensions; no age, agensgraph, apache_age, sqlgMeasured - pg_available_extensions, exact-name match
pgrouting 3.4.1 available, requires postgis; 209 pgr_* functionsMeasured - catalogue query, then CREATE EXTENSION ... CASCADE and a pg_proc count
pgrouting algorithms run on a non-geospatial graph with integer idsMeasured - Dijkstra, components and bridges over a generated 100000/400000 graph, no geometry column
Depth-3 CTE 0.22 ms indexed against 167.59 ms unindexed, 602 ms buildMeasured - EXPLAIN ANALYZE median, cold run discarded, both index states
pgr_dijkstra column contract and ANY-INTEGER typingDocumented - pgRouting manual7
Extracted/source 0.048 to 1.0885, aggregate 0.7633, TOAST 0.3965Measured - seven documents extracted and loaded, byte counts from relation size
Edge Function ceiling 2504695 B; Worker ceiling 14034445 BMeasured - same fixtures and parser on both runtimes
The Worker failure is memory, not CPUMeasured - GraphQL analytics reported exceededMemory; the 128 MB isolate limit is documented13
Date.now() does not advance during Worker CPU executionDocumented14 and observed as 0 ms extract time
Storage serves HTML as text/plain with nosniffMeasured - response headers plus a screenshot of raw source rendering
SQL/PGQ committed 2026-03-16, rewritten in the rewriterDocumented - commit message and commitfest entry69
PG18 is the newest supported major seriesDocumented10
Apache AGE has PG17/PG18 release candidates and a PG19 branchDocumented - upstream tags and branches8
pgpdf parses via poppler; pg_ocr needs plpython3uDocumented1112
Only plpgsql and plpgsql_check procedural languages availableMeasured - catalogue query for ^pl
Maximum disk size for a single projectNot discoverable via the Management API. Billing/addons surfaces probed again 2026-08-11 (3 endpoints; only throughput fields exist). The ceiling question stays open - the experiment project itself measured 88% disk full at the 2026-08-11 audit
Person/organisation extraction qualityMeasured - 561 persons + 2284 orgs extracted live 2026-08-11; a 24-row sample showed name truncation, newline-split labels and sentence over-capture. NOISY, recorded as a finding rather than tuned toward plausible
20 of 1521 citation entities span 2+ documents (18 in two, 2 in three)Measured - cross_document_entities() over the loaded corpus, 2026-08-11
Dates on edges, registry-identifier resolution, the 111-document real-pipeline runMeasured, 2026-08-14 - doc_date on mentions and edges via a BEFORE INSERT trigger, a hub entity resolving to 0 neighbours as at 2020-01-01 and 171 as at 2026-12-31; 148 ABN-shaped candidates gated by the mod-89 checksum to 14 valid mentions and 3 entities; 103 AU council documents loaded and extracted (292 persons / 123 organisations, 31.6 s wall clock for re-extract plus edge build, per-genre ratios 0.0011-0.0362, TOAST 0.4893)
Hub traversal semantics on a real co-occurrence graphMeasured - a degree-917 hub turned the path-set (UNION ALL) depth-3 walk into 65721.220 ms server-side; the level-set (UNION) walk answers the same query in 154.924 ms warm, same project, 2026-08-14
Edge-cached read surfaceMeasured - the worker caches POST bodies under synthetic GET cache keys, 6 h freshness via x-cached-at, serve-stale on origin failure; x-pggraph-cache HIT/MISS/STALE verified on the deployed domain, 2026-08-14
Functions created by hand in a live database do not survive a rebuildMeasured - demo.documents() existed in no committed file, only in an older live project, and a fresh rebuild shipped a UI whose corpus table 404’d (PGRST202). It is defined in demo/db/04-api.sql now
Browser-level end-to-end proof of the deployed demoMeasured - Playwright suite (demo/e2e/) over the live domain: landing, as-at bridging, entity graph + timeline + deep-link persistence, the registry-pin search flow, and the origin-down diagnostic state
Recall or quality of any vector indexNot measured. Synthetic vectors cannot support a recall claim
halfvec is 0.5042 of vector on identical columns; HNSW build 6950 ms and query 0.43 ms against IVFFlat build 4130 ms and query 46.13 ms at lists=100Measured - G05, 30000 chunks x 1536 dimensions, synthetic values, 2026-08-10; storage, build time and latency are value-independent, recall is not
Disk throughput is a separate addon field, not a function of disk size: baseline_disk_io_mbs=347, max_disk_io_mbs=2085Measured - G06, /projects/{ref}/billing/addons, 2026-08-10
Cost per document: about 430 KB source to about 11 KB logical to about 5.4 KB on disk for council minutes; a 4 TB corpus at about $92/mo against about $239/mo at the US federal ratioMeasured inputs (G12, 2026-08-14) times Supabase list prices dated 2026-08-10; compute not priced
Scanned / image-only PDFsDetection measured - an image-only NARA scan extracted 0.5 chars/page against 2710-3242 for born-digital references, so ~270 chars/page routes a document to OCR. OCR itself remains unavailable in-database: plpython3u absent from pg_available_extensions, verified by query 2026-08-11
Behaviour under concurrent traversalMeasured - depth-3 CTE at concurrency 1/4/16/64 on the 100000/400000 graph: p50 0.29 ms aggregate, p95 ~3 ms, 0 errors in 85 queries, same 2-vCPU medium, 2026-08-11. pgrouting algorithms remain single-query

  1. Supabase, database extensions. https://supabase.com/docs/guides/database/extensions

  2. pgRouting manual. https://docs.pgrouting.org/latest/en/index.html

  3. PostgreSQL, recursive queries with WITH. https://www.postgresql.org/docs/current/queries-with.html

  4. Supabase, GraphQL via pg_graphql. https://supabase.com/docs/guides/graphql 2

  5. PostgreSQL, ltree. https://www.postgresql.org/docs/current/ltree.html 2

  6. PostgreSQL commit, SQL Property Graph Queries (SQL/PGQ), 2026-03-16. https://www.postgresql.org/message-id/E1w247I-0000Tk-2Y%40gemulon.postgresql.org 2 3

  7. pgRouting, pgr_dijkstra, including the Edges SQL inner-query contract. https://docs.pgrouting.org/latest/en/pgr_dijkstra.html 2 3

  8. Apache AGE. https://age.apache.org/ 2

  9. PostgreSQL commitfest entry 4904, closed Committed in PG19-Final. https://commitfest.postgresql.org/patch/4904/ 2

  10. PostgreSQL, supported version list. https://www.postgresql.org/versions.json 2

  11. pgpdf, a PDF type for Postgres, parsing via poppler. https://pgxn.org/dist/pgpdf/ 2

  12. pg_ocr, OCR in Postgres via PL/Python3U and ONNX Runtime. https://github.com/Z-Xiao-M/pg_ocr 2

  13. Cloudflare Workers, platform limits. https://developers.cloudflare.com/workers/platform/limits/ 2

  14. Cloudflare Workers, security model - timers only advance after I/O. https://developers.cloudflare.com/workers/reference/security-model/ 2

  15. pgmq, a Postgres message queue. https://github.com/pgmq/pgmq

  16. pg_cron, a job scheduler for Postgres. https://github.com/citusdata/pg_cron

  17. Supabase Queues. https://supabase.com/docs/guides/queues

  18. Supabase, automatic embeddings - the documented queue-plus-consumer architecture. https://supabase.com/docs/guides/ai/automatic-embeddings

  19. Fly.io, autostop and autostart machines. https://fly.io/docs/launch/autostop-autostart/

  20. Fly.io regions. https://fly.io/docs/reference/regions/

  21. Supabase, “Manage your usage: disk size,” Supabase Docs. https://supabase.com/docs/guides/platform/manage-your-usage/disk-size

  22. Supabase, “Storage pricing,” Supabase Docs. https://supabase.com/docs/guides/storage/pricing