Database Schema architectural case study¶
atlas.database-schema is an optional full-stack plugin that requires
atlas.standard-catalog. It provides a deliberately narrow example of adding
data and UI to an existing catalog entity without changing that entity's
definition. Follow Build your first plugin for the minimal
route-and-navigation plugin first; return here when deciding whether a Facet
and entity-detail contributions fit your feature.
The product-facing behavior is documented in the Database Schema feature guide. This page explains the architectural choices behind it.
Decision 1: keep schema data independent of the Resource kind¶
The plugin owns a DatabaseSchema Facet with a one-to-one record associated
with the catalog entity, not with Standard Catalog's ResourceDetails model.
The record holds the dialect, SQL input, parsed schema, and parse status. This
keeps the data independent of a frontend contribution and the Resource kind's
core details under the Standard Catalog plugin's ownership.
Choose this shape when an optional plugin owns an aspect of an existing entity.
Do not create a new Entity Kind merely to add such an aspect, and do not put
the plugin's fields in another plugin's details model. The focused endpoint
tests in plugins/database-schema/backend/atlas_plugin_database_schema/tests/test_views.py
cover creating the facet, parsing each supported dialect, conflicts, and its
absence before creation.
The Database Schema plugin registers no Entity Kind, capability, or permission
of its own. Standard Catalog declares schema.host.v1 for Resources; schema
writes reuse the owning Resource's existing edit authority. Its descriptor
therefore declares the Standard Catalog dependency but has no runtime
registration hook.
Decision 2: target contributions by capability, not kind name¶
The frontend contributes Schema and ER Diagram detail tabs. Both use
the schema.host.v1 capability predicate, rather than kind === 'Resource'.
That makes the extension usable by a future kind that explicitly supports the
same contract, while keeping it hidden from entities that do not.
The contribution ids are namespaced under atlas.database-schema, and the ER
diagram tab requests full-width layout. The tab contract is tested in
plugins/database-schema/frontend/src/entityDetailTabs/resource.test.ts: it
checks both tabs appear for a capability-declaring entity, disappear without
the capability, and are not limited to a hard-coded kind.
Use this pattern when a contribution depends on a declared capability. Use a kind-specific condition only when the contract genuinely belongs to that kind; see Choose an extension mechanism for the selection guide.
Decision 3: give plugin-owned data a plugin-owned API¶
The facet is served through the namespaced endpoint
/api/plugins/atlas.database-schema/resources/<id>/schema/, not generic
entity CRUD. The endpoint is included only when the optional plugin is
selected, and it creates, reads, or updates the facet independently of the
Resource's kind details. A missing facet is a normal 404 state for the tabs,
not evidence that the Resource is invalid.
This boundary is appropriate when a plugin owns the data lifecycle and schema. It avoids extending a core endpoint with optional-plugin fields. See Extension points and capabilities when the API must collaborate with another plugin rather than own its own resource.
Decision 4: preserve input when parsing fails¶
Saving SQL parses it synchronously for PostgreSQL, MySQL, or MS SQL. A successful save stores structured data for the ER diagram; a failed parse preserves the SQL input and records a failed parse state so the author can correct it. The plugin does not connect to or execute against a live database.
For derived data, retain the user's source, make the derived state explicit, and avoid silently discarding either. The feature guide explains the user-facing recovery path.
Decision 5: register a facet-writer instead of being imported¶
register_runtime() registers this plugin's apply/clear implementation into Ingestion's
atlas.ingestion.facet_writers.v1 extension point under key 'database-schema', rather than
Ingestion importing this plugin directly. apply reuses the same parse_schema() the manual
editor already uses; clear removes the Facet. Both run in a second pass, after a Resource's own
entity fields are already upserted through the Entity Service — the Facet write still never goes
through the Resource kind handler (Decision 1 above still holds).
Resolving 'database-schema' and finding nothing registered — this plugin isn't selected for the
distribution — is Ingestion's signal to skip the write entirely; there is no capability check,
because schema.host.v1 describes what a kind could support, not whether this plugin is
currently active (a distribution selecting Standard Catalog without Database Schema would have the
capability declared but no table migrated for it). See Ingestion: extension points it
publishes for the other side.
Once a Resource is YAML-managed, its Facet also stops accepting manual writes through this
plugin's own POST/PATCH — a narrow, local check added to this controller alone (not a
generalized "facets are protected once YAML-managed" mechanism), since the central enforcement
every other kind of YAML-managed data relies on lives in the Entity Service, which this
plugin's endpoint never goes through by design (Decision 3). The check is unconditional on the
entity's source_kind, not on whether the current manifest declares spec.databaseSchema this
run: a Resource that becomes YAML-managed freezes its existing Facet immediately, even before its
manifest ever declares the field. See the feature guide
for the operator-facing behavior.
Choose this shape — a keyed extension point the dependent plugin registers into, rather than the owning plugin importing the dependent one — when an optional plugin needs to react to another plugin's core pipeline without that pipeline acquiring hard-coded knowledge of an open-ended set of optional consumers.
What to reuse¶
- Choose a Facet when your plugin owns an optional aspect of an existing entity.
- Register into another plugin's published extension point instead of being imported by it, so resolving your key and finding nothing registered stays a reliable "not selected" signal rather than a capability check.
- Gate frontend contributions with the capability contract they need.
- Give independently owned data a namespaced plugin API and focused tests.
- Keep product workflows in the feature guide, while this case study remains the explanation of the extension decisions.