Frequently asked questions¶
Grounded answers to the questions this project actually raises. Every function, table, role, GUC, and SQLSTATE named below exists in the shipped extension. Cross-links point to the deep documentation for each topic.
- Installation and access
- Licensing
- OKF and the catalog model
- Identifiers and schema design
- Storage tiers and getting files back
- Search
- Multi-tenancy and version history
- Companion tools
- Interoperability and portability
- PostgreSQL support and operations
Installation and access¶
Why does a fresh login role get a permission error (SQLSTATE 42501)?¶
Because the pgokf schema is not granted to PUBLIC. During
CREATE EXTENSION pgokf, the bootstrap step runs
REVOKE ALL ON SCHEMA pgokf FROM PUBLIC and grants USAGE only to the three
extension roles. A brand-new login role is a member of none of them, so its
first call into the schema fails with SQLSTATE 42501
(insufficient_privilege).
The fix is a single GRANT that makes the login role a member of the right tier:
GRANT pgokf_reader TO app_reader; -- search + read the catalog
GRANT pgokf_writer TO ingest_bot; -- register/refresh bundles (inherits reader)
GRANT pgokf_admin TO catalog_ops; -- config + exports (inherits writer)
All three roles (pgokf_reader, pgokf_writer, pgokf_admin) are created
NOLOGIN: they are privilege buckets, not accounts. The hierarchy is
pgokf_reader < pgokf_writer < pgokf_admin, each tier inheriting the one
below, so a writer can search and an admin can ingest without a second grant.
See Security for the full model.
Do I need superuser to install or use the extension?¶
Installing (CREATE EXTENSION pgokf) requires the usual privilege to create an
extension. Day-to-day use does not require superuser: bundle registration,
refresh, search, and configuration all run through the three pgokf roles. The
functions that touch the server filesystem (register_bundle, refresh_bundle,
export_parquet, export_sources) are SECURITY DEFINER with a hardened
search_path, and are constrained by the allowed_roots policy - see
Security and Configuration.
The bundle path is on my laptop but registration fails - why?¶
The path argument to register_bundle is opened by the PostgreSQL server
process, not your client. It must be an absolute path the server can reach and
must canonicalize (no .. traversal, no symlink escape). If allowed_roots is
configured, the canonical path must also resolve inside one of those roots. A
path that is not registerable raises a validation error; a path already
registered (by the current tenant) raises SQLSTATE 23505 (use refresh_bundle
to re-sync it instead). See
Deployment topologies for how to make bundle files
reachable by the server, including the mountless path
(register_bundle_content plus the pgokf-ingest companion) when the server
cannot mount the files at all.
Licensing¶
What license is pgokf under?¶
AGPL-3.0-only, dual-licensed with a commercial option. Every crate in the
repository (the extension, its okf-parser / okf-sync libraries, and the
companion tools pgokf-ingest, pgokf-embed, pgokf-mcp and pgokf-web,
and the shared pgokf-pgconn, pgokf-companion and pgokf-workspace
libraries)
is licensed under the GNU Affero General Public License, version 3.0 only. A
separate commercial license is available for use the AGPL does not permit,
such as embedding pgokf in a proprietary product or offering it as a hosted
service without releasing your modifications. No part of the project is
permissively licensed. See
LICENSING.md
for the details and the commercial contact.
Does my application become AGPL by connecting to a pgokf database?¶
Building applications that merely connect to a PostgreSQL server with pgokf
installed does not by itself obligate you to release your application's source,
and neither does running unmodified pgokf inside your organization. The AGPL's
network clause applies when you distribute a modified pgokf or make a
modified version available to others over a network. This is a summary, not
legal advice; the license text governs. See
LICENSING.md.
OKF and the catalog model¶
What does "OKF v0.2 conformance" actually mean here?¶
It means the catalog model matches the OKF v0.2 spec's data model:
- Every concept must carry a
type;title,description,resource, andtagsare recommended and projected as first-class columns onpgokf.concepts. - The provenance/trust/lifecycle families -
sources,generated(by/at),verified[]events,status,stale_after, andusage_window- are parsed and projected into dedicated tables rather than being flattened away. - Actors follow the OKF convention
<producer>/<version>,human:<id>, orprocess:<id>in fields likegenerated.byandverified[].by. - The bundle root's reserved
index.mdsupplies the bundle'sokf_version(stored onpgokf.bundles.okf_version); the reserved per-directorylog.mdactivity logs are projected intopgokf.bundle_logwithout ever becoming concepts. Theokf_version_policyconfiguration key decides whether a bundle declaring an unsupported OKF version is warned about or rejected. - An
Attested Computationconcept's spec-mandated reference fields become typed, traversable graph edges (see the next answers).
Producer-defined frontmatter keys that are not part of the standard projection
are not discarded - they are retained per concept in pgokf.concept_metadata
as jsonb, one row per key, and are indexed for containment queries. See
OKF authoring.
How is provenance modeled in the tables?¶
Provenance is split by cardinality across three tables so each shape stays queryable:
pgokf.concept_provenance- one sparse row per concept carrying the scalar generation/trust/lifecycle fields:generated_by,generated_at,status,stale_after,usage_window_from/to,trust_tier, plus a losslessdetailsjsonb.pgokf.concept_verification- theverified[]event list, one row per event (verified_by,verified_at,ordinal).pgokf.concept_provenance_source- thesources[]materials a concept was derived from, one row per source (source_id,resource,title,author,usage_count,last_modified,usage_window_from/to).
All three cascade from pgokf.concepts, so unregistering a bundle or dropping a
concept removes its provenance automatically. See the SQL API for
column-by-column detail.
What is an "Attested Computation" concept?¶
In OKF v0.2, Attested Computation is the one concept type with
type-specific fields (attesting how a computed artifact was produced). pgokf
treats type as data - it is a projected column and a search weight, not a
hard-coded enum - so an Attested Computation concept is ingested like any
other, with two conformance additions:
- Its three reference-bearing fields (
computation,executor,attester, each a bare resource path or a{resource: ...}mapping) are resolved intopgokf.linksas typed edges carrying alink_relationofattestation:computation/attestation:executor/attestation:attester, andpgokf.concept_neighborstraverses a resolved internal one like any other edge. - Its non-reference fields (
runtime,parameters) are retained as producer metadata inconcept_metadata, like any non-modeled key.
See the authoring guide and the glossary.
Are reserved files ingested as concepts?¶
No. index.md and log.md are reserved at every directory level and never
become searchable concepts, and they never count toward a bundle's
file_count. Each is still put to use: the bundle root's index.md supplies
okf_version, and every log.md is parsed line by line into the
pgokf.bundle_log activity-log table (read it with
pgokf.list_bundle_log(bundle_id[, directory])). In the sample bundle,
register_bundle reports four concepts because the reserved index.md is
skipped. See OKF authoring.
Identifiers and schema design¶
Why are bundle IDs bigint instead of UUIDs?¶
pgokf.bundles.id is bigint GENERATED ALWAYS AS IDENTITY. Bundle
registration is a single-writer operation serialized on a per-path advisory
lock, so there is no ID-collision problem to solve, and a monotonic bigint
gives better B-tree index locality and smaller foreign keys - every
concepts, links, concept_metadata, and provenance row carries bundle_id,
so the width matters. A UUID (e.g. UUIDv7) earns its keep only when many
independent writers or federated catalogs must mint IDs without coordinating;
that is not the current single-catalog model. If federation is later needed, the
surrogate key is the natural seam to change. See Architecture.
Concept IDs are strings - where do they come from?¶
A concept's id is derived from its path: the normalized, bundle-relative path
with the .md suffix removed (e.g. runbooks/database-failover). The primary
key on pgokf.concepts is (bundle_id, id), so the same logical path in two
bundles is two distinct concepts. This is why concept_neighbors needs a
bundle_id to disambiguate when an ID exists in more than one active bundle
(it raises SQLSTATE 22023 if you omit it and the ID is ambiguous; disabled and
retired bundles do not count toward the ambiguity).
Storage tiers and getting files back¶
What are the two storage tiers, and which should I use?¶
The store_source config key selects the tier (default false):
store_source = false(default) - data-lake / enterprise tier. PostgreSQL holds metadata, the search index, the link graph, and provenance. The original files stay in their external object store or mounted bucket. Best when the files are large or already live in a data lake, and you want PostgreSQL to be the query layer, not the store of record.store_source = true- small self-contained tier. Each concept's verbatim source bytes are also stored inpgokf.concept_source(compressed with lz4 where the build supports it, else pglz). The database is then a complete, portable install that needs no external file store.
store_source is not retroactive - a change takes effect for bundles synced
or refreshed afterward; existing rows keep their stored source (or its absence)
until refresh_bundle re-indexes them. See
Deployment topologies and
Configuration.
Can I get the original files back out of PostgreSQL?¶
Only when they were stored (store_source = true). Two functions read them back:
pgokf.get_concept_source(bundle_id, concept_id)returns the exact bytes of one stored concept file (bytea). Reader-level.pgokf.export_sources(bundle_id, dest_dir)writes a bundle's stored originals back to a directory on the server. Admin-only, and constrained by the sameallowed_rootspath policy as registration.
The stored bytes are verbatim: they hash back to pgokf.concepts.file_hash
(BLAKE3). With store_source = false these functions have nothing to return -
the files live only in your external store. Every read/export through these
paths (and export_parquet) also appends one row to the access audit
(pgokf_private.access_log, read with the admin-tier pgokf.list_access_log).
See the SQL API and Operations.
Search¶
Is search real BM25, or PostgreSQL FTS?¶
Shipped search is native PostgreSQL full-text search - no third-party
extension. pgokf.concept_search builds a query with websearch_to_tsquery and
ranks with ts_rank_cd against a weighted tsvector (body_tsv): title
weight A, tags/type/description weight B, body weight D. It returns a
ts_headline snippet per hit. The tsvector is indexed with GIN
(concepts_body_tsv_gin).
BM25 is available as an optional, config-selected backend, not a separate
function. Setting the durable search_backend key to bm25 routes the same
pgokf.concept_search through a provider's bm25 index (Tiger Data
pg_textsearch, PostgreSQL license, or ParadeDB pg_search) - the operator
must install the provider separately, and search falls back to native FTS
with a warning when it is absent. There is no standalone pgokf bm25() function; it
is a backend mode selected by configuration. See
Enabling the BM25 backend and the
search_backend key.
When does native FTS become a problem, and what does BM25 buy?¶
Measured on this project:
- Selective queries - point lookups, tag filters, type filters - stay sub-millisecond to roughly 10-15 ms even at ~10M concepts, because they ride B-tree / GIN indexes.
- Broad "rank everything" full-text queries scale linearly with corpus
size: about 322 ms at 1M, 2.4 s at 10M, and 29 s at 50M concepts,
because
ts_rank_cdmust score every match.
For broad top-k queries over very large corpora, the optional bm25 backend
(search_backend=bm25, backed by Tiger Data pg_textsearch or ParadeDB
pg_search) prunes with BM25 top-k instead of scoring the whole match set, so
broad queries stay roughly flat where native ranking grows linearly. It
requires the operator to install a provider and falls back to native FTS when
none is present. So native FTS is the
right default for selective queries and moderate corpora; enable the bm25
backend when you need broad top-k at scale. See
Enabling the BM25 backend,
Benchmarks, and the Search guide.
How do I search within one bundle, filter, or paginate?¶
concept_search(query, bundle_id => NULL, limit_count => 20, ...). Pass a
bundle_id to scope the search to one bundle; limit_count must be in the range
1..=500 (otherwise SQLSTATE 22023). Search only ever touches active
bundles (enabled and not retired). Four optional structured-filter arguments
(concept_type, tags as ALL-of containment, status, trust_tier) narrow
the ranked set, each a no-op when NULL, and the trailing after_cursor jsonb
argument gives exact keyset pagination: copy the rank, bundle_id, and
concept_id of a page's last row into it and the next page continues strictly
after that row, with no OFFSET drift. pgokf.search_facets counts the same
matching set grouped by one facet (type, bundle, status, trust_tier, or
tag), and pgokf.find_similar ranks concepts against a seed concept's most
salient lexemes. See the Search guide.
Does pgokf do semantic (vector) search?¶
Yes, as an optional surface built on pgvector, mirroring the optional BM25
seam (CREATE EXTENSION pgokf succeeds without pgvector):
- A companion (the shipped
pgokf-embed, or your own) computes embeddings outside PostgreSQL and streams them in through the writer-tierpgokf.set_concept_embedding(bundle_id, concept_id, embedding real[]); the extension never computes embeddings or performs network I/O. The expected vector length is the durableembedding_dimconfig key (default 1536). pgokf.concept_search_semantic(query_embedding, ...)ranks by pgvector cosine distance (therankcolumn is the normalized cosine similarity). It requires pgvector and raises SQLSTATE22023naming the missing dependency when it is absent; there is no lexical fallback.pgokf.concept_search_hybrid(query, query_embedding, ...)fuses the lexical and semantic result lists with Reciprocal Rank Fusion (RRF, k = 60) entirely in SQL, and degrades to lexical-only with aWARNINGwhen pgvector is absent.pgokf.rebuild_embedding_index()(admin-tier) builds a pgvector HNSW cosine index for the configured dimension.
See Semantic and hybrid search
and the embedding_dim key.
Why doesn't my query language config change affect existing rows?¶
The default_text_search_config key (default pg_catalog.english) is applied
when a concept's tsvector is built at index time and when a search query is
parsed. Changing it affects bundles synced or refreshed afterward; existing
rows keep their tsvector until refresh_bundle re-indexes them. It must name
an installed configuration (validated against pg_catalog.pg_ts_config). See
Configuration.
Multi-tenancy and version history¶
Does pgokf support multi-tenancy or row-level security?¶
Yes: opt-in row-level security keyed on the pgokf.tenant session GUC,
shipped since 0.1.7 and backward compatible by construction:
- A session that never sets
pgokf.tenant(the default) sees every row and behaves exactly as before multi-tenancy existed (unless therequire_tenantpolicy is on, which denies it); a session that sets it (SET pgokf.tenant = 'acme', per role viaALTER ROLE, or as a connection option) reads only that tenant's rows. - Writes are stamped and confined:
register_bundle/register_bundle_contentstamp the bundle from the session tenant, every projected child row inherits it, and a tenant-set session that targets a bundle owned by another tenant gets the same22023"bundle ... is not registered" it would get for a nonexistent bundle. - Bundles are keyed
UNIQUE (tenant_id, path), so two tenants may register the same path independently.
One honest caveat: pgokf.tenant is a USERSET GUC, so it is a scoping
selector, not a hard security boundary against a tenant who can run arbitrary
SQL: any session can SET or RESET it, and the unset default sees all rows
(unless require_tenant is on).
A hard boundary requires a constrained access layer that pins the GUC, or a
per-tenant-role / per-database model. See Multi-tenancy and
Security.
Can I see what a concept said last Tuesday?¶
Yes, with opt-in version history (since 0.1.11). Enable the track_history
config key and every subsequent sync records an append-only SCD Type-2 version
trail in pgokf.concept_history: an added concept starts at version 1, an
update closes the open version and appends the next, and a removal appends a
tombstone. Read it with pgokf.concept_history(bundle_id, concept_id) (the
timeline, newest first) and pgokf.concept_as_of(bundle_id, concept_id, as_of)
(the single version valid at an instant: the point-in-time answer). The
feature is off by default with zero storage cost, is not retroactive
(recording begins at the next sync), and history_retention_days bounds growth
by pruning closed versions older than the window. See
Version history.
Companion tools¶
What are the companion binaries, and do they run inside PostgreSQL?¶
Several standalone crates ship alongside the extension. All of them run out of process, keep network and object-store credentials outside PostgreSQL, and reach the catalog only through its public SQL functions:
pgokf-ingest: mountless ingestion. Lists an S3-compatible object store (MinIO / SeaweedFS / AWS S3 / GCS / Azure), downloads the objects, and streams them topgokf.register_bundle_contentas apgokf_writerrole. A--watchmode re-lists on an interval (default 60 s) and re-ingests when the content hash changes.pgokf-embed: the reference embedding generator for semantic search. Finds concepts without embeddings, calls a configurable OpenAI-compatible/v1/embeddingsendpoint, and streams each vector back throughpgokf.set_concept_embedding.pgokf-mcp: a Model Context Protocol server exposing the catalog to AI agents over stdio JSON-RPC (tools backed byconcept_search,find_similar,concept_neighbors, and a concept getter), or over HTTP (--http) with a bearer token per request and a role deciding which tools it may call.pgokf-web: the web UI and JSON API over the catalog - search, browsing, concept pages, link graphs, an agent plugin builder, and, once a writer connection and an identity mode are configured, the human workflow of upload, edit and review.pgokf-pgconn: the shared connect helper the companions use; it adds optional TLS to PostgreSQL (--tls, envOKF_PG_TLS, orsslmode=require).
See Deployment topologies for the ingestion companion in context.
Interoperability and portability¶
Can I export the catalog for use in other tools?¶
Yes. pgokf.export_parquet(bundle_id, dest_dir) snapshots a bundle's projection
to four Parquet files - concepts.parquet, concept_metadata.parquet,
links.parquet, and concept_provenance.parquet - in a server-side directory,
returning per-file row counts and total bytes (pgokf.export_result). The
output was verified interoperable with DuckDB on this project: you can point
DuckDB straight at the files and query them. export_parquet is admin-only and
its destination is validated exactly like a bundle root (absolute,
traversal-free, inside allowed_roots). See Operations.
Is the bundle on disk still authoritative after I import it?¶
Yes - that is the design. The Markdown bundle is the portable source of truth;
the pgokf schema is a projection you can rebuild at any time by re-running
refresh_bundle. Incremental sync re-parses only files whose BLAKE3 content
hash changed and removes rows for deleted files, so the catalog converges to
whatever is on disk. See Architecture. (A content-sourced
bundle, one streamed in with register_bundle_content, has no on-disk root;
re-sync it by calling register_bundle_content again.)
PostgreSQL support and operations¶
Which PostgreSQL versions are supported?¶
PostgreSQL 15, 16, 17, 18, and 19 from one codebase (pgrx feature flags
pg15–pg19; CI builds the matrix). See Packaging.
Which optional extensions does pgokf use?¶
Four, all runtime-only and all degrading cleanly when absent;
CREATE EXTENSION pgokf never requires any of them:
- pgvector enables
concept_search_semantic(which raises22023without it) and the semantic half ofconcept_search_hybrid(which degrades to lexical-only with aWARNING). pg_textsearch(Tiger Data, PostgreSQL license, PostgreSQL 17-18) orpg_search(ParadeDB, AGPL-3.0) enables thesearch_backend=bm25mode (one per database;bm25_providerchooses);concept_searchfalls back to native FTS with a warning without one.pg_cronenablesschedule_refresh/unschedule_refresh;schedule_refreshraises22023naming the missing dependency without it.
pgokf.search_index_status() reports which of them are installed and how much
of the catalog their indexes cover. See Operations.
Is sync_log_retention_days doing anything?¶
Yes, since 0.1.5. Every successful sync appends one row to the admin-only
pgokf_private.sync_log audit trail (read via pgokf.list_sync_log), and
immediately afterward history older than sync_log_retention_days (default
30; 0 keeps it forever) is pruned in the same transaction. Since 0.1.8 the
same window also governs the per-concept change manifest
(pgokf_private.sync_log_change, which cascades from sync_log) and the
access/exfiltration audit (pgokf_private.access_log). See
Configuration.
Still stuck?¶
- Exact function signatures and column lists: SQL API
- Error-to-SQLSTATE mapping with fixes: Troubleshooting
- Term definitions: Glossary