Sidecars

Build with Sidecars

Select, configure and integrate existing Sidecars with a tested optional capability, visible fallback and clean lifecycle.

Alpha documentation

Build around an existing application's validated operations, then add only the capabilities it needs. This guide takes an optional documentation tab from Library selection to a callable interface, with a visible browser fallback and a clean unmount.

Before you begin

Open the intended project and application in cApp. Have a source entry point and choose your target platform. Use Projects and Build for that setup; use Create a shell Sidecar when you are writing a new component instead of consuming one.

The TypeScript example assumes your app already bundles @capp/app-sdk and its contracts dependency. Follow the source-workspace SDK instructions to build and install those packages. A bare package import is not a browser URL, and this guide does not assume an npm-registry release exists. No wallet, model, backend credential or provider account is needed for the example.

1. Select for one application and target

In Library, choose Add to app and Configure for the intended application. Build's picker may call the first action Add to build. Check implementation state and target support, configure the component, then review Selected and any Needs notices. Library configuration saves as fields change; closing Done is not a separate save operation.

For this example, add Links to a supported Windows or Android package. Leave the documentation destination as a normal HTTPS URL. Compile or select your application's immutable release, inspect the effective Sidecar selection, then package and install. A saved selection does not modify a binary already installed. Follow recompile versus repackage when deciding which step changed.

Keep selection small. Guide can run authored help without Local AI; Local AI adds inference. Market Pulse can provide data without a wallet. Conduit can perform configured reads without the entire Edge composition. The directory also contains planned entries and a core Android authentication coordinator: neither should be treated as an ordinary selectable runtime merely because it has a page.

Selection tools & JSON exports app selection metadata, not the package source or an installed provider. It is not a universal selection-import workflow. User package import is a different Library action. Review configuration before sharing an export; endpoints and app-specific values may be private.

Match the identifier to the interface

NameMeaningExample
Package IDWhat Library selects for an appuk.capp.sidecar.links
SDK capabilityWhat the app waits forlinks
Runtime methodWhat the resolved interface exposeslinks.open(url, options)
Agent toolA separately registered broker operationUse the component's Agent reference, not a JavaScript method guess

Do not pass a package ID to require() expecting installation or automatic namespace mapping. The SDK has a defined set of typed capability mappings. An arbitrary custom capability string is not a generic property lookup; a custom shell global needs an explicitly implemented bridge. Solana's thin client and the component-specific references describe their own interfaces.

2. Await a declared capability

The SDK reads the runtime exposed to this application's window. It does not need a hard-coded local service port. In an embedded app, use the interface supplied to that registered surface; do not reach into unrelated parent frames or start another shell provider.

Use these elements in your page. The ordinary link remains usable without JavaScript or a cApp runtime:

html
<button id="open-docs" type="button" disabled>Open docs in cApp</button>
<p id="docs-status" role="status">Checking in-app tabs…</p>
<a href="https://c-app.uk/developer-docs/"
   target="_blank" rel="noopener noreferrer">Open documentation in a browser</a>

Call mountDocsButton() after the elements exist. It returns a disposer for your router or component unmount. Replace the example appId with your application's identity; it is a label/contract identity, not a credential or permission grant.

ts
import {createCappClient, defineCappApplication} from '@capp/app-sdk';

export function mountDocsButton(root: ParentNode = document): () => void {
  const button = root.querySelector<HTMLButtonElement>('#open-docs');
  const status = root.querySelector<HTMLElement>('#docs-status');
  if (!button || !status) throw new Error('The documentation controls are missing.');

  const client = createCappClient({
    application: defineCappApplication({
      appId: 'com.example.reader',
      optional: {links: '>=1'}
    })
  });
  let disposed = false;
  let busy = false;
  const canOpen = () => typeof client.capability('links')?.open === 'function';
  const refresh = () => {
    if (disposed) return;
    button.disabled = busy || !canOpen();
    if (!busy) status.textContent = canOpen()
      ? 'In-app tabs are available.'
      : 'In-app tabs are unavailable. Use the browser link below.';
  };
  const unwatch = client.watchCapability('links', refresh);

  const open = async () => {
    if (disposed || busy) return;
    busy = true;
    button.disabled = true;
    status.textContent = 'Requesting a documentation tab…';
    try {
      await client.ready();
      const links = await client.require('links', 5000);
      if (disposed) return;
      if (typeof links.open !== 'function') throw new Error('Unsupported Links interface.');
      await links.open('https://c-app.uk/developer-docs/', {mode: 'tab'});
      if (!disposed) status.textContent = 'Tab requested. Check the tab for the result.';
    } catch (_error) {
      if (!disposed) status.textContent = 'The tab request failed. Use the browser link below.';
    } finally {
      busy = false;
      if (!disposed) button.disabled = !canOpen();
    }
  };
  button.addEventListener('click', open);
  return () => {
    disposed = true;
    unwatch();
    button.removeEventListener('click', open);
    button.disabled = true;
  };
}

// Run once after the page is mounted; call disposeDocs() when it is removed.
const disposeDocs = mountDocsButton();

Only the button click requests a tab. Repeated clicks while that request is pending are ignored; a rejection is handled whether open() fails synchronously or asynchronously. The fallback is a separate user action, not an automatic browser launch after an ambiguous result. Dispose before remounting to avoid duplicate listeners. Disposing stops observation and further UI work; it cannot undo an already submitted tab request.

What readiness does and does not prove

CheckMeaningNot a guarantee of
client.ready()Waits for the exposed runtime's readiness promise and checks the supplied app contract when applicableEvery optional provider being ready; a rejected host readiness promise being propagated
client.compatibility()Compares declared requirements with the current descriptorA live provider health check or a permission grant
client.has('links')The mapped interface is presentCorrect configuration, successful navigation or every method working
client.require('links', 5000)Waits for the interface, with a bounded capability waitInstalling/enabling the package or timing out the operation subsequently called
client.watchCapability(...)Observes interface presence/replacement and returns an unsubscribe functionA stream of provider-internal health changes

An ordinary browser can return a ready descriptor in web-fallback mode with no native capabilities. Optional requirements do not fail compatibility; handle their absence yourself. Required capability mismatches in a present runtime raise CAPP_RUNTIME_INCOMPATIBLE. A missing capability can raise CAPP_CAPABILITY_TIMEOUT in a window, or CAPP_CAPABILITY_UNAVAILABLE without a window. Do not turn a ready descriptor into an unconditional enable button. For a required wallet or execution capability, stop the operation rather than using this optional navigation fallback.

3. Keep business rules in the application

A Guide action must call your application's normal validated operation, not a second unsafe path. A market quote does not authorise a trade. A policy pass does not grant wallet authority. Public content describing a tool does not grant an Agent permission to use it.

Separate read, configure, approve and execute actions in your UI. Treat runtime configuration as distinct from build-time defaults. Keep secrets out of client source, distributable manifests, examples, selection exports and screenshots. Only use a client-visible credential when the provider explicitly permits that distribution; privileged upstream credentials belong behind an authorised service boundary.

For external effects, persist the operation identity and exact intent before execution, record the returned evidence, and reconcile an unknown outcome. Do not retry a payment merely because the reply was lost. The Solana recovery guide explains the concrete operation/attempt model; the SDK's generic capability discovery does not implement universal business-operation recovery for you.

4. Respect ownership and lifecycle

Use the selected provider's owning API rather than constructing one with unrelated defaults. Distinguish not selected, selected but still starting, disabled, misconfigured, and failed. A missing selected provider must not silently become a substitute, a different network or a weaker approval policy. The component reference determines which explicit standalone/provider modes are supported.

Do not destroy a shell-owned provider when one app view closes. Release only resources your view acquired: capability watchers, event listeners, timers and subscriptions. For Market Pulse, acquire() returns a release function and subscribe() returns an unsubscribe function. Keep both and call them on unmount. Native UI belongs in the shell; a child application bridge should not mount a second copy of it.

Plan for provider replacement as well as startup. Re-resolve the interface at action time; do not retain an obsolete provider forever after one successful readiness event. Before an effect, inspect that component's own status, selected network and approval requirements.

5. Test the installed composition

Start with a harmless operation and a small target-specific build. Use this acceptance matrix in addition to application unit tests:

CaseObservable result
Selected, configured and installedThe intended provider responds; the app shows its actual result
Sidecar absent or still startingOptional UI explains the fallback; required actions remain unavailable
Provider rejects or disconnectsA visible error; no secret-bearing diagnostic dump or automatic substitute
View closes during a pending requestNo later view mutation or duplicate listener; an external effect is not assumed cancelled
Reopen/reload/restartUI recovers from current state, not an assumed previous success
Wrong network, declined approval or unknown effectExecution stops or reconciles according to the component contract
Target unsupported or component plannedNo claim that documentation or selection supplies the missing runtime

A standard browser preview can test app layout and intentional web fallback. A Studio shell preview can test a component's mapped surface. A Showcase may use a declared fixture/browser composition. None proves that an installed native capability, wallet handoff or external provider works. Update the package to test new native Sidecar selections; check desktop and touch layouts separately.

Troubleshooting

SymptomCheck next
require() times outCorrect app selection, supported target, installed package version and runtime ownership; use the SDK capability name, not the package ID
has() is true but a call failsMethod support, component status, configuration, network and permissions; interface presence is only the first check
Settings appear unchangedLibrary edits target the next package; check any scoped Guide/Studio release override and which binary is running
Component cannot be selectedPlanned/core classification, target support and Library's app identity
Duplicate panels or callsDuplicate mount/listener, missed disposer, or a child frame incorrectly creating an owner provider
Tab request succeeds but the page does not loadCheck the actual tab and destination restrictions; use the ordinary browser link deliberately

What success looks like

Your app requests only the intended Sidecars, checks the interface it actually receives, and gives a useful response when an optional one is absent. Required authority never falls back silently. An installed test confirms the selected target and provider; closing a view does not leak subscriptions or disrupt another consumer.

Create your own capability

The Library also accepts validated user-authored shell packages. Follow Create a shell Sidecar for the public scaffold, local preview, package boundary, import workflow and API-ownership rules. Component-specific integrations continue in Guide, Local AI, Market Pulse and Solana.

↑ ↓ to navigate · Enter to open · Esc to close. Search stays in your browser.