How to Add a Custom Provider to CodexBar: A 3-Step Integration Guide
You add a custom provider to CodexBar by defining a JSON declaration in providers.json, registering a unique ProviderInstanceID with the ProviderDescriptorRegistry, and implementing a CustomHTTPJSONFetcher that respects the security contracts for HTTPS-only requests and environment-based secrets.
CodexBar discovers every usage provider through the ProviderDescriptorRegistry, which bootstraps a ProviderDescriptor for each value of the closed UsageProvider enum at launch and stores it in a thread-safe Store. To add a custom provider that lives outside this compile-time enum, you must extend the runtime architecture using the three-step integration path defined in the repository's design documentation, as implemented in steipete/CodexBar.
How ProviderDescriptorRegistry Manages Providers
The ProviderDescriptorRegistry serves as the central authority for provider discovery in CodexBar. At application launch, it executes ensureBootstrapped() to populate the descriptorsByID list with static descriptors mapped from the UsageProvider enum (see Sources/CodexBarCore/Providers/ProviderDescriptor.swift, lines 55-89).
Because UsageProvider is a closed enum, you cannot add cases at runtime. Instead, the architecture provides a runtime seam that allows you to register custom descriptors without modifying the enum. Custom providers appear in the UI automatically because PreferencesProvidersPane iterates over ProviderDescriptorRegistry.all, which includes both compiled and runtime-registered descriptors.
Step-by-Step: Adding a Custom HTTP JSON Provider
The integration path involves three distinct steps: defining the provider in JSON, registering its instance ID, and wiring the fetcher implementation.
Step 1: Define the Provider in providers.json
Create a JSON definition following the schema outlined in docs/custom-provider-design.md (lines 50-80). Place this block in your providers.json file or user configuration file read by SettingsStore.
The JSON defines a ProviderInstanceID (lowercase ASCII, 1-64 characters), the HTTP request configuration, and a declarative mapping for transforming the JSON response into a ProviderFetchResult.
{
"version": 2,
"providers": [
{
"id": "acme-gateway",
"kind": "custom-http-json",
"label": "Acme Gateway",
"enabled": true,
"request": {
"method": "GET",
"url": "https://gateway.example.com/v1/quota",
"authentication": { "type": "bearer" }
},
"mapping": {
"primary": {
"usedPercent": { "path": "quota.used_pct" },
"resetsAt": { "path": "quota.reset_at", "dateFormat": "iso8601" },
"windowMinutes": { "path": "quota.window_minutes" }
},
"cost": {
"used": { "path": "spend.usd" },
"currency": "USD",
"period": "Approx. spend"
},
"identity": {
"organization": { "path": "plan.name" },
"loginMethod": { "literal": "api" }
}
}
}
]
}
This JSON decodes into a ProviderDefinition (future type) that lives alongside the existing ProviderConfig struct in Sources/CodexBarCore/Config/CodexBarConfig.swift (lines 109-132).
Step 2: Register the Instance with ProviderDescriptorRegistry
Extend ProviderDescriptorRegistry with a static registerCustom(_:instanceID:) method that inserts the custom descriptor into the store without touching the UsageProvider enum. This method creates a ProviderDescriptor using the shape defined in ProviderDescriptor.swift (lines 27-38).
extension ProviderDescriptorRegistry {
/// Register a custom-http-json provider that was loaded from the user config.
public static func registerCustom(
id: ProviderInstanceID,
config: CustomHTTPJSONConfig
) -> ProviderDescriptor {
// Build a descriptor that uses the custom fetch plan.
let descriptor = ProviderDescriptor(
id: .custom, // Note: UsageProvider.custom does not exist; we keep the enum untouched.
metadata: ProviderMetadata(
displayName: config.label,
weeklyLabel: "",
isPrimaryProvider: false,
tokenCost: ProviderTokenCostConfig(supportsTokenCost: false,
noDataMessage: { "N/A" })
),
branding: ProviderBranding(iconStyle: .default, color: .gray),
tokenCost: ProviderTokenCostConfig(supportsTokenCost: false,
noDataMessage: { "N/A" }),
fetchPlan: .customHTTPJSON(config: config), // New case added to ProviderFetchPlan
cli: ProviderCLIConfig(name: config.id, aliases: [])
)
return register(descriptor)
}
}
Step 3: Implement the CustomHTTPJSONFetcher
Implement a CustomHTTPJSONFetcher that conforms to ProviderFetcher and respects the security contracts: HTTPS-only URLs, no redirects, secrets via environment variables (CODEXBAR_CUSTOM_<INSTANCE_ID>_API_KEY), 2xx response codes, and a 1 MiB body limit. The fetcher consumes the mapping paths and produces a ProviderFetchResult.
struct CustomHTTPJSONFetcher: ProviderFetcher {
let config: CustomHTTPJSONConfig
let instanceID: String
func fetch(context: ProviderFetchContext) async throws -> ProviderFetchResult {
// 1️⃣ Validate URL (HTTPS, no user-info, no fragment)
let url = try URL(validating: config.request.url)
// 2️⃣ Resolve secret from env: CODEXBAR_CUSTOM_<INSTANCE_ID>_API_KEY
let secret = ProcessInfo.processInfo.environment["CODEXBAR_CUSTOM_\(instanceID.uppercased())_API_KEY"]
var request = URLRequest(url: url)
request.httpMethod = config.request.method
if config.request.authentication.type == .bearer {
request.setValue("Bearer \(secret ?? "")", forHTTPHeaderField: "Authorization")
}
// 3️⃣ Perform GET with a 15s timeout, 1MiB cap
let session = URLSession(configuration: .ephemeral)
let (data, response) = try await session.data(for: request, delegate: nil)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw ProviderError.httpFailure
}
// 4️⃣ Decode JSON and apply the declarative mapping (paths defined in config.mapping)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let snapshot = try CustomMappingEngine.apply(json: json, mapping: config.mapping)
return ProviderFetchResult(snapshot: snapshot)
}
}
Wire this fetcher into ProviderFetchPlan by adding a new customHTTPJSON case in Sources/CodexBarCore/Providers/ProviderFetchPlan.swift. When ProviderRegistry.specs(...) builds a ProviderFetchContext, it will use your custom fetcher instead of the built-in ProviderFetchPlan used by first-party providers.
Security and Runtime Constraints
Custom providers must adhere to strict security boundaries defined in the architecture:
- HTTPS only: The fetcher must reject HTTP URLs and validate TLS certificates.
- Environment secrets: API keys must be passed via the environment variable
CODEXBAR_CUSTOM_<INSTANCE_ID>_API_KEY, never hardcoded in the JSON. - Response limits: The fetcher must enforce a 1 MiB body limit and a 15-second timeout.
- No redirects: The URLSession must disable automatic redirects to prevent SSRF attacks.
How the UI Recognizes Custom Providers
Once registered, custom providers appear automatically in the preferences UI. The PreferencesProvidersPane (located in Sources/CodexBar/PreferencesProvidersPane.swift) iterates over ProviderDescriptorRegistry.all, which includes both enum-based and runtime-registered descriptors. The existing menu items, widgets, and CLI commands continue to work because they address providers through the abstract ProviderDescriptor interface.
Summary
- Define your provider in
providers.jsonfollowing the JSON schema fromdocs/custom-provider-design.md, specifying theid,request, andmappingfields. - Register the
ProviderInstanceIDby extendingProviderDescriptorRegistrywith aregisterCustommethod that creates aProviderDescriptorwith a.customHTTPJSONfetch plan. - Implement a
CustomHTTPJSONFetcherthat validates HTTPS URLs, reads secrets from environment variables, enforces response limits, and maps JSON toProviderFetchResult. - Consume the provider normally—the UI, CLI, and widgets treat custom providers identically to first-party providers because they use the abstract
ProviderDescriptorinterface.
Frequently Asked Questions
Can I add a custom provider without modifying the CodexBar source code?
Currently, no. The concrete types (CustomHTTPJSONConfig, CustomHTTPJSONFetcher, and the registration helper) are not yet implemented in the repository. Adding a custom provider requires extending the codebase as described above, though the architecture in ProviderDescriptorRegistry and ProviderFetchPlan already anticipates this extension.
How does the CodexBar UI display custom providers?
The PreferencesProvidersPane automatically displays custom providers because it iterates over ProviderDescriptorRegistry.all, which returns every registered descriptor regardless of whether it originated from the UsageProvider enum or runtime registration. The UI requires no changes to support custom providers.
What authentication methods are supported for custom HTTP JSON providers?
According to the design document, custom providers support Bearer token authentication via the authentication.type field in the JSON configuration. The secret must be provided through the environment variable CODEXBAR_CUSTOM_<INSTANCE_ID>_API_KEY, and the fetcher constructs the Authorization: Bearer header accordingly.
Where is the custom provider design documented?
The complete specification lives in docs/custom-provider-design.md within the repository. This document defines the JSON schema, security boundaries (HTTPS-only, no redirects, 1 MiB limit), and the runtime seam for registering providers outside the UsageProvider enum.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →