* docs: outline plugin architecture and connector design * feat(den-api): add plugin architecture admin and webhook foundation * refactor(den-api): rename plugin routes and split github connector env * fix(den-api): scope plugin connector records by organization --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
15 KiB
New Plugin Arch Data Structure
This document holds the proposed data model and schema direction for the new plugin architecture.
It is intentionally separate from prds/new-plugin-arch/plan.md so the plan can stay focused on product direction and architectural decisions while this file captures implementation-oriented structure.
Type-specific shape docs live in:
prds/new-plugin-arch/config-types/README.md
API design lives in:
prds/new-plugin-arch/api.md
RBAC design lives in:
prds/new-plugin-arch/rbac.md
Guiding rules
- config objects are first-class and versioned;
- plugins link to config object identities, never directly to object versions;
- plugin resolution always uses the latest active object version;
- latest version is derived from
config_object_versionordering, not stored separately onconfig_object; - key config payload/data columns should be encrypted at rest;
- friendly current metadata like
titleanddescriptioncan remain plaintext for UI and search; - connector provenance is stored explicitly;
- deletes are soft and path tombstones are preserved for connector-managed items;
- RBAC shape should stay consistent across config objects, plugins, and connectors.
Core tables
config_object
Stable identity row for one logical config object.
Suggested columns:
idorganization_idobject_type(skill,mcp,command,agent,hook,context,custom)source_mode(cloud,import,connector)titledescriptionnullablesearch_textnullableslugor stable org-local key if neededcurrent_file_namenullablecurrent_file_extensionnullablecurrent_relative_pathnullablestatus(active,inactive,deleted,archived,ingestion_error)created_by_org_membership_idconnector_instance_idnullablecreated_atupdated_atdeleted_atnullable
Notes:
- this is the row plugins reference;
- this is also the row current search and dashboard queries should hit;
- title/description on this row are the current projection derived from the latest version, not an independent historical source of truth;
title,description, andsearch_textmay remain plaintext because they are intended for dashboard rendering and search;updated_atis convenience metadata only and should not be treated as the source of truth for latest version resolution;- connector-managed objects still use the same identity table.
config_object_version
Immutable content/history row for each version of a config object.
Suggested columns:
idconfig_object_idnormalized_payload_jsonraw_source_textnullableschema_versionor parser version nullablecreated_via(cloud,import,connector,system)created_by_org_membership_idnullableconnector_sync_event_idnullablesource_revision_refnullableis_deleted_versionboolean default falsecreated_at
Notes:
- object-type-specific fields should generally live in payload JSON, not as many sparse columns on the shared table;
- version rows should not be the primary surface for current library search because that would create duplicate hits across historical versions of the same object;
- a deleted source file can create a terminal deleted version while leaving the identity row intact;
normalized_payload_json,raw_source_text, and any equivalent key content columns for config objects should be encrypted at rest;config_object_versionis the single source of truth for version history and latest-version lookup.
Current metadata projection rule:
- after creating a new latest version, parse whatever title/description/friendly metadata can be derived from that version and write the current projection onto the parent
config_objectrow; - current dashboard/search experiences should query
config_object, notconfig_object_version.
Suggested index:
- (
config_object_id,created_atDESC,idDESC)
Latest lookup rule:
- latest version for an object = newest row for that
config_object_id, ordered bycreated_at DESC, id DESC. created_atshould be database-generated so ordering stays authoritative.
Version number note:
- v1 does not require a separate version-number column on
config_object_version; - immutable ids plus
created_atare enough for history and latest-version resolution; - add a human-facing version number later only if product UX needs ordered revision labels.
Metadata extraction note:
- some config types derive title/description from file contents, such as skill frontmatter;
- other config types may derive friendly metadata from file name, path, or type-specific parsing rules;
- type-specific extraction rules should run when projecting the latest version onto
config_object.
plugin
Stable deliverable row.
Suggested columns:
idorganization_idnamedescriptionstatuscreated_by_org_membership_idcreated_atupdated_atdeleted_atnullable
Notes:
- a plugin is the administrator-facing unit of delivery;
- a plugin contains config object identities, not pinned content versions;
- when resolving a plugin, the system selects the newest version row for each linked object.
plugin_config_object
Membership join between plugins and config object identities.
Suggested columns:
idplugin_idconfig_object_idmembership_source(manual,connector,api,system)connector_mapping_idnullablecreated_by_org_membership_idnullablecreated_atremoved_atnullable
Constraints:
- unique active membership on (
plugin_id,config_object_id)
Notes:
- current implementation keeps one logical membership row per (
plugin_id,config_object_id) and usesremoved_atfor soft removal/reactivation rather than append-only history rows; - if an object later becomes deleted, the membership row can remain while delivery skips that object.
Access and RBAC tables
We want the same RBAC model across config objects, plugins, and connectors.
There are two realistic schema options:
- Separate access tables per resource type
- better foreign keys
- more repeated schema
- One generic access table
- easier shared logic
- weaker relational guarantees
Current recommendation:
- start with separate tables that share the same shape.
plugin_access_grant
Suggested columns:
idplugin_idorg_membership_idnullableteam_idnullableorg_wideboolean default falseroleorpermission_levelcreated_by_org_membership_idcreated_atremoved_atnullable
config_object_access_grant
Same shape as plugin access, but scoped to config_object_id.
connector_instance_access_grant
Same shape as plugin access, but scoped to connector_instance_id.
RBAC note:
- plugin delivery may be implemented primarily by plugin access grants;
- if a team has access to a plugin, that is effectively the publish step.
- config objects and plugins should be private by default;
- sharing with the whole org should be represented as one org-wide grant, not per-user entries.
- use
org_wide = truefor v1. - member and team sharing should continue to use normal explicit grant rows.
- current implementation also uses one logical grant row per target principal and reactivates it by clearing
removed_at.
Connector tables
connector_account
Represents one authenticated or installed connector relationship.
Examples:
- one GitHub App installation
- one future API credential binding
Suggested columns:
idorganization_idconnector_type(github, etc.)remote_idexternal_account_refdisplay_namestatuscreated_by_org_membership_idcreated_atupdated_at
Notes:
- secrets should stay out of git-backed repo files and remain private;
idis OpenWork's local primary key, whileremote_idis the stable connector-side identifier we can use across different connector families;- this row is the reusable "one-time setup" layer.
connector_instance
Represents one configured use of a connector account.
Examples:
- a GitHub repo + branch configuration
- a future API collection endpoint mapping
Suggested columns:
idorganization_idconnector_account_idconnector_typeremote_idnullablenamestatusinstance_config_jsonlast_synced_atnullablelast_sync_statusnullablelast_sync_cursornullablecreated_by_org_membership_idcreated_atupdated_at
Notes:
- one connector instance may feed multiple plugins;
- one plugin may include objects from multiple connector instances;
- one connector instance may ingest objects without direct plugin auto-membership;
remote_idis optional here because some connector instances may not map cleanly to one remote object, while others will.
connector_target
Represents the external source target inside an instance.
Examples:
- repo owner/name
- branch
- API endpoint family
- collection identifier
Suggested columns:
idconnector_instance_idconnector_typeremote_idtarget_kindexternal_target_reftarget_config_jsoncreated_atupdated_at
Notes:
- this table lets us support git and non-git connectors with one shared abstraction;
remote_idshould be the canonical external identifier for the target, such asorg/repofor GitHub repo targets.
connector_mapping
Maps part of a connector target into a config object type and optional plugin behavior.
Suggested columns:
idconnector_instance_idconnector_target_idconnector_typeremote_idnullablemapping_kind(path,api,custom)selectorobject_typeplugin_idnullableauto_add_to_pluginbooleanmapping_config_jsoncreated_atupdated_at
Examples:
- selector
/sales/skills/**->skill-> plugin A - selector
/sales/commands/**->command-> plugin A - selector
/finance/skills/**->skill-> plugin B
Notes:
- this is the row that captures the default parent-path -> plugin behavior;
- advanced/manual plugins can still include connector-managed objects outside this automatic mapping;
remote_idcan be used if a connector exposes mapping-level remote identifiers, but it is optional.
connector_sync_event
Audit row for each webhook/poll/sync execution.
Suggested columns:
idconnector_instance_idconnector_target_idnullableconnector_typeremote_idnullableevent_typeexternal_event_refnullablesource_revision_refnullablestatussummary_jsonstarted_atcompleted_atnullable
Notes:
- useful for debugging, replay decisions, and ingestion history;
- for GitHub this should also capture delivery ids and head commit SHAs inside
summary_jsonor promoted columns if we need faster filtering.
connector_source_binding
Links a live config object identity to its external source location.
Suggested columns:
idconfig_object_idconnector_instance_idconnector_target_idconnector_mapping_idconnector_typeremote_idnullableexternal_locatorexternal_stable_refnullablelast_seen_source_revision_refnullablestatuscreated_atupdated_atdeleted_atnullable
Examples of external_locator:
- repo path
- API resource id
- remote document key
Notes:
- one live object should normally have one active source binding;
- this is how we know which external path/resource created the object;
remote_idcan hold a stable connector-native file/resource id when the remote system provides one.
connector_source_tombstone
Preserves deleted source locations so we do not accidentally revive old identities.
Suggested columns:
idconnector_instance_idconnector_target_idconnector_mapping_idconnector_typeremote_idnullableexternal_locatorformer_config_object_iddeleted_in_sync_event_iddeleted_source_revision_refnullablecreated_at
Notes:
- if the same path later reappears, ingestion creates a new config object identity;
- this table prevents accidental reconnect of a recreated file to an old object.
Optional release/install tables
We have not finalized delivery yet, but these are likely candidates.
plugin_release
Optional first-class release/snapshot row for a plugin.
Suggested columns:
idplugin_idrelease_kind(manual,system,access_change,sync_snapshot)created_by_org_membership_idnullablecreated_atnotesnullable
plugin_release_item
Snapshot of the config object versions included at release time.
Suggested columns:
idplugin_release_idconfig_object_idconfig_object_version_idcreated_at
Notes:
- even if runtime delivery is rolling latest, these tables can still be useful for audit, rollback, and support;
- if we decide releases are unnecessary, these tables can be deferred.
Suggested write patterns
Creating a new cloud/import object
- insert
config_object - insert first
config_object_version - parse current metadata from that version and update
config_object.title,description,search_text, and any current file metadata - optionally update
config_object.updated_at - optionally insert
plugin_config_object
Connector sync updating an existing object
- create
connector_sync_event - locate active
connector_source_binding - insert new
config_object_version - parse current metadata from that version and update the parent
config_objectprojection - optionally update
config_object.updated_at - update
connector_source_binding.last_seen_source_revision_ref
Connector sync deleting an object
- create deleted
config_object_versionor mark identity status as deleted - update
config_object.statusand clear or adjust current searchable projection as needed - close
connector_source_binding - insert
connector_source_tombstone - keep
plugin_config_objecthistory intact
Latest-version strategy
To keep one source of truth and avoid out-of-date derived state, we should not store latest_version_id on config_object in v1.
Instead:
- treat
config_object_versionas the only source of truth for version ordering; - determine latest by query using
created_at DESC, id DESC; - keep version rows immutable.
Example lookup pattern:
select v.*
from config_object co
join config_object_version v on v.config_object_id = co.id
where co.id = ?
order by v.created_at desc, v.id desc
limit 1;
Why this is the current recommendation:
- no duplicated latest-version pointer to drift out of sync;
- no revision counter race condition;
- simple write path;
- acceptable read cost given expected version counts and proper indexing;
- current library search can still stay fast because it queries
config_object, not historical versions.
Future option:
- if reads later prove too expensive, we can add a derived latest pointer as an optimization, but not as the authoritative source of truth.
Current schema recommendation
If we had to start implementation now, the minimum useful table set would be:
config_objectconfig_object_versionpluginplugin_config_objectplugin_access_grantconnector_accountconnector_instanceconnector_targetconnector_mappingconnector_sync_eventconnector_source_bindingconnector_source_tombstone