Reverse-engineered by reading the nova-codex-curator source directly
(SQLAlchemy model classes, dataclasses, OpenSearch index-creation code, and
S3/YAML/JSON structures) rather than authored by the team that owns this
service. Review, if needed, happens via PR against this file like any other
schema change.
Derived from:
curator/pipelines/servicenow_kb/db.py — the only real relational schema
(SQLAlchemy DeclarativeBase models against a PostgreSQL "global tenant
catalog", read via psycopg2/asyncpg). No Alembic/migrations folder
exists in the repo — the tables are declared in code only and presumably
provisioned/migrated by whatever owns the shared global DB (not this repo).
curator/clients/opensearch.py — the AWS OpenSearch k-NN vector index
definition (ensure_index/create_index), which is this service's actual
"document" schema (chunk + embedding + metadata).
curator/models.py and curator/pipelines/servicenow_kb/models.py — plain
@dataclass types (not ORM/DB-backed) used to move data between pipeline
stages in memory; some of their fields end up as S3 object metadata and/or
OpenSearch document fields, so they're documented here as the shape that
feeds the persisted stores.
curator/pipelines/crawler/gov_regulations/{config_loader,base,manifest}.py
— YAML-configured crawl sources and a JSON sync-state manifest, both
persisted as objects in S3 rather than in a database. Included here as
"schema" because they are structured, versioned records this service reads
and writes on every run.
curator/api.py — an in-memory (non-persisted, process-local) job-status
dict exposed over GET /sync/{job_id}.
curator/config.py — Pydantic settings (CODEX_ env prefix); not schema
itself but shows which of the above stores are wired up (opensearch_index,
bedrock_embedding_dimension, global_database_url, s3_bucket).
There is no single unified database: this service spans a relational
tenant catalog (Postgres), a search/vector index (OpenSearch), and
object-store-as-record-store (S3 JSON/YAML/metadata). Each is documented as
its own cluster below.
Service dependencies (per pyproject.toml / docs/architecture.md):
AWS S3 (raw store), AWS OpenSearch (k-NN vector index), AWS Bedrock (Claude
vision for OCR, Cohere embed-v4 for embeddings), PostgreSQL via SQLAlchemy +
psycopg2/asyncpg (multi-tenant ServiceNow OAuth catalog), FastAPI/uvicorn
(HTTP API), semantic-text-splitter (chunking), ServiceNow Table API
(external, JWT-bearer OAuth, tenant knowledge-base source).
erDiagram
CODEX_DOCUMENTS {
string _id PK "= chunk_id"
text content "chunk text"
float_vector embedding "knn_vector, dim=1024"
object metadata "flat bag of provenance fields, see detail below"
}
One index per "channel": the default codex-documents index (gov
regulations / generic crawler / union docs), plus one index per country for
gov regulations (gov_regulations_{country}, e.g. gov_regulations_us) and
one index per tenant for ServiceNow KB sync (index name = tenant_id).
All indices share the identical mapping shown above — OpenSearchClient is
parameterized by index_name and create_index() is called with whichever
name the caller passes in.
Cluster 3 — S3-persisted structured records (JSON / YAML, not a DB)¶
App/client name; this service filters to app = "superagent_jwt" only — other rows (other apps' OAuth clients) are presumed to coexist in the same table but are out of scope for this service.
client_id
String
OAuth client id used as JWT iss/aud.
client_secret
Text
OAuth client secret. Stored possibly Node.js AES-256-CBC-encrypted (iv:ciphertext format, detected by presence of a :); decrypted at read time via decrypt_node_format() using Settings.encryption_key.
Composite primary key: (sn_instance, app). This table is a shared
global catalog — the comment in db.py says tenant config is loaded "from
the global PostgreSQL DB", implying this Postgres instance/schema is owned
and populated by another service (likely the Superagent platform side), not
by nova-codex-curator itself; this repo only reads it.
codex-documents (OpenSearch index; default name codex-documents, overridable per country/tenant)¶
Field
Type
Notes
_id
keyword (doc id)
Set to chunk.chunk_id ({document_id}#chunk_{seq}) on bulk index.
HNSW method, space_type: innerproduct, engine: faiss. Produced by AWS Bedrock Cohere embed-v4 (Settings.bedrock_embedding_model_id).
metadata.chunk_id
keyword
Same value as _id.
metadata.document_id
keyword
SHA-256-derived id of the source document; used for "does this document already exist" checks and for delete-by-document.
metadata.article_number
keyword
ServiceNow KB article number (KB sync path only).
metadata.article_sys_id
keyword
ServiceNow KB article sys_id (KB sync path only).
metadata.source_url
keyword
Original URL the content was fetched from.
metadata.source_domain
keyword
Domain used for the raw/{domain}/… S3 prefix and for delete-by-domain.
metadata.filename
keyword
Original filename, where applicable.
metadata.page_count
integer
PDF page count, where applicable.
metadata.agency
keyword
Issuing agency (gov-regulations path).
metadata.jurisdiction
keyword
e.g. "US Federal".
metadata.country
keyword
Country code (gov-regulations path).
metadata.state
keyword
Optional state/province filter.
metadata.language
keyword
ISO language code, default en.
metadata.publication_date
keyword
Source publication/amendment date.
metadata.crawled_at
keyword
ISO timestamp of crawl/sync.
metadata.kb_type
keyword
ServiceNow knowledge_types "type" field, remapped from type.
metadata.kb_type_sys_id
keyword
ServiceNow knowledge_types "type_sys_id" field, remapped from type_sys_id.
metadata.description
text
Free-text description, where present.
metadata.agent_policy
boolean
Flag surfaced from ServiceNow KB metadata (meaning not confirmed — see Open Questions).
metadata.cfr_title
keyword
eCFR title number (gov-regulations / us_ecfr handler).
metadata.cfr_part
keyword
eCFR part number (gov-regulations / us_ecfr handler).
Note: the mapping declares a fixed metadata property list, but
bulk_index()/_sync_article() actually write metadata as {**base, **chunk.metadata} / an arbitrary dict — i.e. additional ad-hoc keys (e.g. tenant_id, kb_sys_id, kb_title, article_title, category, chunk_index, total_chunks, synced_at) are written into metadata at index time even though they aren't declared in the explicit mapping above. OpenSearch will dynamically map those extra fields (unless dynamic mapping is otherwise restricted), so the effective document shape is broader than the declared mapping — see Open Questions.
Index settings: knn: true, number_of_shards: 1, number_of_replicas: 0
(same for every index/tenant/country variant — no sharding-for-scale
strategy per index).
Not a DB row — this shape is what ends up as S3 object metadata
(x-amz-meta-*) on the raw object, per the s3_meta dict built in
_sync_article() and the crawler's upload path.
From instances.sn_instance, e.g. https://mycompany.service-now.com.
client_id
str
From instance_oauth.client_id (app=superagent_jwt).
client_secret
str
Decrypted from instance_oauth.client_secret.
private_key
str
PEM RSA key from instances.private_key.
verifier_kid
str
From instances.verifier_kid.
user_id
str
Default ""; ServiceNow sys_id used as JWT sub, optionally overridden per sync run.
This is an in-memory join of tenants ⋈ instances ⋈ instance_oauth — not
a table itself, but documented because it's the shape the rest of the
pipeline (ServiceNow client, syncer) actually consumes.
Used to decide whether a re-fetch is needed (needs_update()).
crawled_at
str
ISO timestamp of last crawl.
document_id
str
Document id (matches RawDocument/chunk document_id).
Stored as a flat JSON object keyed by part_id → PartState fields (see
example in docs/architecture.md). Not a table; one manifest object per
(country, source_id) pair in S3.
Copied from SyncStats.errors, or the top-level exception message on crash.
started_at / finished_at
str | None
ISO timestamps.
No persistence: this dict lives in a single global dict guarded by a
threading.Lock, lost on process restart, and not shared across replicas —
worth flagging if this service is expected to run with >1 replica or survive
restarts mid-job (see Open Questions).
Open questions / things to confirm before treating this as the reviewed contract¶
No migrations for the Postgres tables.instances / tenants /
instance_oauth are declared as SQLAlchemy models in this repo, but there
is no Alembic/migrations/ directory here — meaning either (a) this repo
only reads a schema owned/migrated by another service, or (b) migrations
live somewhere not found in this checkout. Confirm which service owns
provisioning this Postgres schema (likely a Superagent platform-side
repo), since nova-codex-curator's model classes could silently drift
from the real DDL.
instance_oauth.app values beyond superagent_jwt. The table clearly
supports multiple OAuth "apps" per instance (composite PK includes app),
but only superagent_jwt is read here — unclear what other app values
exist or who else writes/reads this table.
OpenSearch mapping vs. actual indexed fields mismatch.create_index()
declares a fixed set of metadata.* sub-fields, but bulk_index()/
_sync_article() write additional keys (e.g. tenant_id, kb_sys_id,
kb_title, article_title, category, chunk_index, total_chunks,
synced_at) that aren't in the declared mapping. Confirm whether dynamic
mapping is intentionally relied on here, or whether the declared mapping
is stale/incomplete.
Meaning of metadata.agent_policy (boolean). It's declared in the
OpenSearch mapping but no code path in this repo sets or reads it — likely
populated by a ServiceNow knowledge_types key not seen in the sample
data, or set by a consumer outside this repo. Needs confirmation from
whoever owns the knowledge_types contract in ServiceNow.
KBArticle.workflow_state and KBBase fields aren't enumerated.
ServiceNow workflow states (e.g. draft/published/retired) aren't
constrained to a known set anywhere in this codebase — treated as a free
string. Confirm the actual choice list against the ServiceNow KB app
(likely nova-superagent-sn-app or similar) if filtering by state
matters downstream.
In-memory-only job state (_jobs in curator/api.py). Not persisted
to any database; lost on restart and not shared across replicas/processes.
Confirm whether this is acceptable for the deployed topology (single
long-lived process vs. multiple replicas/autoscaling), since POST /sync
dedup-by-tenant and GET /sync/{job_id} status lookups both rely on this
process-local dict.
Vector-DB choice differs from the PRD assumption in the task framing.
This service uses AWS OpenSearch k-NN, not pgvector/Chroma/
Pinecone/Weaviate — confirmed directly from curator/clients/opensearch.py
and pyproject.toml (opensearch-py). No pgvector or other vector-DB
client library is present in dependencies.
S3 "schema" (manifests, configs, raw prefixes) has no schema
versioning.SyncManifest/PartState and the YAML SourceConfig are
parsed with dataclass(**dict)/yaml.safe_load with no version field, no
validation beyond required-key KeyErrors, and no migration story if the
shape changes — a stale S3 manifest from an older code version could load
incorrectly (e.g. via PartState(**val) if fields are added/removed).
Worth flagging as a forward-compat risk, not confirmed as a design intent.
Legacy single-tenant ServiceNow settings (servicenow_instance,
servicenow_username, servicenow_password, servicenow_kb_id in
curator/config.py) are marked "kept for backward compat" but no code
path reading them was found in the files inspected — confirm whether
they're dead config or used by a code path not covered in this pass
(e.g. a legacy CLI entry point).