AD-008: Connector System
Summary
Connectors are Bowrain's primary integration mechanism. They reach into live
systems — CMS, design tools, code repositories, marketing platforms, and
file systems — and move content bidirectionally into and out of
the ContentStore. Server-side connectors implement IntegrationConnector
(Fetch/Publish). Client-side connectors implement SourceConnector
(Push/Pull). The kapi CLI with the bowrain plugin is itself a File
connector, managing local files and syncing with the server via REST.
Context
A content platform is only as useful as the systems it integrates with. Treating file formats as the primary integration mechanism produces export/import workflows that are brittle, manual, and disconnected from the source system. Changes in the CMS require re-export; translations sit in files until someone remembers to re-import; no live connection exists between the content source and the translation environment.
Connectors to native tools — pulling data directly into a versioned store and publishing translations back — produce a fundamentally better workflow. Content flows bidirectionally through a unified platform instead of living in exchange files that drift out of sync.
Two concerns need separate interfaces:
- Server-side connectors reach outward from Bowrain into external systems. Fetch content from WordPress; publish translations back to WordPress. The terminology is Bowrain's perspective.
- Client-side connectors act from a source system's perspective. Push source files to Bowrain; pull translated files back. This is what the bowrain CLI does for local files, and what other source systems (code repository bots, design plugin bridges) do for their environments.
Decision
Two Interfaces, One Base
All connectors share a common identity and lifecycle base:
type ConnectorBase interface {
ID() string
Name() string
Category() Category
Status(ctx context.Context) (*SyncStatus, error)
Configure(config map[string]string) error
Close() error
}
Server-side connectors implement IntegrationConnector — Bowrain reaches
into an external system:
type IntegrationConnector interface {
ConnectorBase
// Fetch retrieves source content FROM the external system INTO Bowrain.
Fetch(ctx context.Context, opts FetchOptions) ([]*ContentItem, error)
// Publish sends translated content FROM Bowrain TO the external system.
Publish(ctx context.Context, items []*ContentItem, opts PublishOptions) error
}
Client-side connectors implement SourceConnector — the source system
drives sync:
type SourceConnector interface {
ConnectorBase
// Push sends source content FROM the source system TO Bowrain.
Push(ctx context.Context, opts PushOptions) (*PushResult, error)
// Pull retrieves translated content FROM Bowrain TO the source system.
Pull(ctx context.Context, opts PullOptions) (*PullResult, error)
}
The terminology split resolves the ambiguity between "push from bowrain CLI to server" and "push translations to WordPress" — the two operations are the same word from opposite directions.
Connector Categories
The category enum classifies the integration space:
type Category string
const (
CategoryFile Category = "file" // Filesystem — the kapi CLI, server-hosted directories
CategoryCode Category = "code" // Git repositories and forges
CategoryCMS Category = "cms" // Content management systems
CategoryDesign Category = "design" // Design tools
CategoryMarketing Category = "marketing" // Marketing platforms
CategoryTMS Category = "tms" // External TMS integrations (reserved)
)
CategoryTMS is reserved — the constant is declared, but no
connector currently registers under it. The connectors that ship today
are file (file), git (code), forge (code), WordPress (cms),
Figma (design), and HubSpot (marketing); the category list names the
integration space the registry classifies, not a set of shipping
integrations. External TMS integration is a future slot in that space
rather than an available connector.
Each populated category has characteristic behaviors: CMS connectors paginate through entries and publish via content APIs; design connectors read text layers and write back translated overlays; code connectors commit to branches and open pull requests; marketing connectors sync campaigns and assets across locales.
The forge connector: the delivery tier
The forge connector is the git connector plus the forge itself (GitHub
or GitLab, cloud or self-managed): source ingestion from the tracked
branch, and delivery as a pull/merge request rather than a direct
push. It is what makes a repository a zero-CI-configuration Bowrain
project:
- Inbound: the forge sends push webhooks to
POST /api/webhooks/forge/<connector-id>— unauthenticated, verified with the connector'swebhook_secret(GitHub signs the body with HMAC-SHA256 inX-Hub-Signature-256; GitLab echoes the secret inX-Gitlab-Token). A verified push to the tracked branch re-ingests the source and publishesconnector.push.completed, so the project converges under its normal on-push policy. Pushes to any other branch — including the connector's own delivery branch — are acknowledged and ignored, which is the loop guard. - Outbound: when a convergence run ends converged or parked
(
convergence.run.completedon the event bus), the server materializes per-locale target files from the block store (each locale's translations promoted into the source position at the conventional target path) and the connector writes them to a stable delivery branch (bowrain/translationsby default, recreated from the tracked tip on every delivery), then creates — or updates in place — the one open pull/merge request for that branch, carrying the convergence report. Once the request merges, the same output stops producing deliveries: the loop terminates instead of re-opening merged work.
Forge API calls (find/create/update the request) live in
bowrain/forge, deliberately thin over net/http. The API token and
webhook secret are sealed at rest like every connector credential, and
the token reaches git over https via GIT_CONFIG_* environment
variables — never argv. Failed and canceled runs deliver nothing.
On GitHub the connector can run in App mode (auth: app): the server
holds one registered GitHub App (app id + private key + webhook secret,
GITHUB_APP_* config), authenticates to its API with a short-lived RS256
JWT, and mints cached per-installation access tokens for each delivery —
connectors then carry no credentials at all, and one app-level endpoint
(/api/webhooks/github-app) receives pushes for every installed
repository, routed to the tracked connector by repository path.
Installing the app on a repository is the only per-repo step. GitLab has
no app equivalent; its connectors use project access tokens.
Because one app serves every workspace, and its JWT can mint a token for
any installation of that app, an installation id carries no tenancy of
its own. forge_installations supplies it: the row binding an
installation to the single workspace that owns it, and the first thing
the post-install setup endpoints consult — an installation a workspace
has not claimed reads as not found, indistinguishable from one that has
never existed. Ownership is written from the two ends of an install,
which arrive independently and in either order. The app-level
installation and installation_repositories deliveries are authentic
but anonymous, so they only ever record an installation, and drop it on
uninstall so a claim cannot outlive the access it was granted. The
signed state minted when a workspace starts an install, and returned by
GitHub on the setup redirect, is what attributes it; first claim wins.
The state is a short-lived JWT under its own audience
(bowrain-setup-state), keeping it and session tokens mutually
unusable.
Options and Status
type FetchOptions struct {
Paths []string
Since time.Time
Filter map[string]string
}
type PublishOptions struct {
Locales []model.LocaleID
DryRun bool
}
type PushOptions struct {
Paths []string // Specific file paths to push (empty = all)
Force bool // Push all blocks, ignoring sync cache
DryRun bool // Report what would be pushed without sending
}
type PullOptions struct {
Locales []model.LocaleID // Target locales to pull (empty = all)
Force bool // Overwrite local changes
DryRun bool // Report what would be pulled without writing
}
type SyncStatus struct {
ConnectorID string
LastSync time.Time
ItemCount int
FileCount int
WordCount int
PendingPull int // Items changed externally since last pull
PendingPush int // Items changed locally since last push
Errors []string
}
type ContentItem struct {
ID string
Name string
Path string
Format string
Locale model.LocaleID
Blocks []*model.Block
Metadata map[string]string
LastChanged time.Time
}
Status() provides a lightweight check without performing a full
Fetch/Pull, powering the sync indicators in the connector management UI
and the kapi status command.
The bowrain plugin as the file connector
The bowrain plugin is the primary SourceConnector implementation —
BowrainSourceConnector. It manages .kapi projects (a kapi.yaml
recipe and sibling .kapi/ state directory) and syncs local files with
Bowrain Server.
.kapi project (recipe + state dir)
│
▼
bowrain CLI (reads recipe content collections)
│
▼
FormatRegistry (HTML, JSON, XLIFF, Markdown, ...)
│
▼
Streaming Pipeline (Parts → Blocks)
│
▼
REST Client
│
▼
Bowrain Server (sync endpoints)
│
▼
ContentStore
The bowrain CLI is to Bowrain Server as git is to GitHub — a local tool that syncs with a remote platform. It does not manage server-side connectors; it does not access the ContentStore directly; it does not run server-side automation. Its job is to extract Blocks from local files, compute content hashes, and move them across the sync boundary (AD-009: Sync Protocol).
Server-Side Connector Architecture
Server-side connectors live in bowrain/connector/. They use the
framework's format and pipeline machinery to normalize external content
into the same Part stream that flows through the rest of the system.
┌─────────────────────────────────────────────────┐
│ Bowrain Server (Platform) │
│ ┌──────────────────────────────────────────┐ │
│ │ ContentStore (AD-004) │ │
│ └──────────────────────────────────────────┘ │
│ ▲ ▲ ▲ ▲ ▲ │
│ │ │ │ │ │ │
│ CMS Design Code Marketing File │
│ Conn. Conn. Conn. Conn. (API) │
└────┼────────┼─────────┼─────────┼─────────┼─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
WordPress Figma GitHub HubSpot kapi CLI
CMS Design Repo Marketing (.kapi project)
Content from any connector flows into the same ContentStore and the same streaming pipeline. Tools, content memory, terminology, AI translation, and review all operate identically regardless of origin.
Registry
Server-side connectors register into a Registry:
type Factory func(config map[string]string) (IntegrationConnector, error)
type Info struct {
Name string
Category Category
}
type Registry struct {
mu sync.RWMutex
factories map[string]Factory
infos map[string]Info
}
func (r *Registry) Register(name string, category Category, factory Factory)
func (r *Registry) NewConnector(name string, config map[string]string) (IntegrationConnector, error)
func (r *Registry) List() []Info
Built-in connectors register via init(). Plugin connectors register at
runtime via gRPC discovery — see
AD-framework-007: Plugin System.
The bowrain CLI does not interact with the server-side registry. It is
a file-based SourceConnector that syncs via REST.
Credential Flow
Workspace-scoped credentials are stored encrypted in the database. When a connector needs OAuth authorization (e.g. Figma, HubSpot), the server initiates an authorization flow:
- User clicks "Connect Figma" in the connector management UI.
- Server redirects to Figma's OAuth endpoint with a workspace-scoped state parameter.
- Figma redirects back to
/connectors/:id/oauth/callback. - Server exchanges the code for access + refresh tokens and encrypts them with the workspace's encryption key.
- Subsequent Fetch/Publish calls load and decrypt the tokens on demand.
For destructive or high-privilege connector operations (Publish that would mutate production content, re-authorize with elevated scopes), the server issues a step-up prompt via the permission system — see AD-003: Permissions and Access Control.
Connector Management UI
The connector management panel in bowrain/apps/bowrain/ and
bowrain/apps/web/ provides:
- Configuration. API keys, endpoints, authentication for each
connector. Connector-specific settings are rendered dynamically based
on the connector's
Configure()schema. - Content browser. Tree/list view of remote content items returned
by
Fetch()with content type icons and locale indicators. - Sync status. Last pull/push timestamps, pending changes count,
change indicators from
Status(). Badges alert when source content has changed since the last pull. - Visual diff. Compare local ContentStore state against remote source to identify what has changed before pulling or pushing.
Format System Integration
Both server-side and client-side connectors use the framework's three-tier format system — see AD-framework-005: Formats:
- Native formats (Go): built-in implementations — HTML, XML, XLIFF, XLIFF 2, JSON, YAML, PO, Properties, Markdown, CSV, SRT, VTT, TMX, and more (see the format reference for the current set).
- Plugin formats (any language): external executables via gRPC.
- Bridge formats (Okapi): subprocess-hosted filters via the gRPC bridge protocol.
All three tiers register into the framework's FormatRegistry.
Connectors use the registry to read and write content based on the item
format detected from MIME type, file extension, magic bytes, or content
sniffing.
Embedded Translation
For design tools and CMS platforms, a lightweight Bowrain panel can be embedded within the host application. The embedded UI is a WebView rendering a subset of the translation editor, connected to the host application's connector via a bidirectional message channel.
When a translator edits a translation in the embedded panel, the change propagates through the connector and the sync protocol to update the ContentStore. When source content changes in the host application, the embedded panel updates to reflect the new content. The connector abstraction extends beyond data exchange to become a full in-context translation experience within native tools.
Consequences
- Content flows bidirectionally between live systems and the ContentStore — no export/import shuffling.
- Role separation is clear: server-side connectors handle external integrations; the bowrain CLI handles local files.
- The bowrain CLI is the File connector and stays focused: it reads and writes local files and syncs via REST. It never manages server-side connectors or accesses the ContentStore directly.
- Plugin connectors register at runtime via gRPC, so new integrations ship without rebuilding the server.
- Connectors are the primary integration mechanism; file formats are a single connector category (File), not the whole story.
- The connector interface uses streaming Parts, the same unit used throughout the pipeline — any connector's output feeds directly into tools, content memory, terminology, and AI processing without adaptation layers.
- Credentials are workspace-scoped, encrypted at rest, and subject to step-up prompts for high-privilege operations.