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
| Name | Meaning | Example |
|---|---|---|
| Package ID | What Library selects for an app | uk.capp.sidecar.links |
| SDK capability | What the app waits for | links |
| Runtime method | What the resolved interface exposes | links.open(url, options) |
| Agent tool | A separately registered broker operation | Use 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:
<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.
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
| Check | Meaning | Not a guarantee of |
|---|---|---|
client.ready() | Waits for the exposed runtime's readiness promise and checks the supplied app contract when applicable | Every optional provider being ready; a rejected host readiness promise being propagated |
client.compatibility() | Compares declared requirements with the current descriptor | A live provider health check or a permission grant |
client.has('links') | The mapped interface is present | Correct configuration, successful navigation or every method working |
client.require('links', 5000) | Waits for the interface, with a bounded capability wait | Installing/enabling the package or timing out the operation subsequently called |
client.watchCapability(...) | Observes interface presence/replacement and returns an unsubscribe function | A 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:
| Case | Observable result |
|---|---|
| Selected, configured and installed | The intended provider responds; the app shows its actual result |
| Sidecar absent or still starting | Optional UI explains the fallback; required actions remain unavailable |
| Provider rejects or disconnects | A visible error; no secret-bearing diagnostic dump or automatic substitute |
| View closes during a pending request | No later view mutation or duplicate listener; an external effect is not assumed cancelled |
| Reopen/reload/restart | UI recovers from current state, not an assumed previous success |
| Wrong network, declined approval or unknown effect | Execution stops or reconciles according to the component contract |
| Target unsupported or component planned | No 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
| Symptom | Check next |
|---|---|
require() times out | Correct 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 fails | Method support, component status, configuration, network and permissions; interface presence is only the first check |
| Settings appear unchanged | Library edits target the next package; check any scoped Guide/Studio release override and which binary is running |
| Component cannot be selected | Planned/core classification, target support and Library's app identity |
| Duplicate panels or calls | Duplicate mount/listener, missed disposer, or a child frame incorrectly creating an owner provider |
| Tab request succeeds but the page does not load | Check 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.