> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blnkfinance.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom App security and best practices

> Security and reliability checklist for Custom Apps before you ship to production in Blnk Cloud.

export const CtaCallout = props => {
  const {title, buttonLabel, href, trackingEvent, buttonTarget, rel = "noopener noreferrer", children} = props;
  const handleCtaClick = () => {
    if (typeof window === "undefined" || !trackingEvent) {
      return;
    }
    try {
      window.dispatchEvent(new CustomEvent("blnk:docs-cta", {
        detail: {
          name: trackingEvent,
          href
        }
      }));
    } catch {}
    try {
      window.posthog?.capture?.(trackingEvent, {
        href
      });
    } catch {}
    const gaPayload = {
      cta_href: href
    };
    try {
      window.gtag?.("event", trackingEvent, gaPayload);
    } catch {}
    try {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: trackingEvent,
        ...gaPayload
      });
    } catch {}
  };
  const isExternal = typeof href === "string" && (/^https?:\/\//i).test(href);
  const target = buttonTarget ?? (isExternal ? "_blank" : undefined);
  const linkRel = isExternal ? rel : undefined;
  return <section className="cta-callout not-prose relative my-8 w-full min-w-0 overflow-hidden rounded-xl border border-zinc-200 p-5 dark:border-white/10">
      <div className="cta-callout-noise" aria-hidden="true" />
      <div className="cta-callout-layout">
        {title ? <div className="cta-callout-title-row">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" width="14" height="14" className="cta-callout-icon shrink-0 text-zinc-800 dark:text-zinc-200" aria-hidden="true">
              <g fill="none" fillRule="nonzero">
                <path d="M28 0v28H0V0h28ZM14.691833333333335 27.134333333333334l-0.012833333333333334 0.0023333333333333335 -0.08283333333333333 0.04083333333333334 -0.023333333333333334 0.004666666666666667 -0.016333333333333335 -0.004666666666666667 -0.08283333333333333 -0.04083333333333334c-0.011666666666666667 -0.004666666666666667 -0.022166666666666668 -0.0011666666666666668 -0.028000000000000004 0.005833333333333334l-0.004666666666666667 0.011666666666666667 -0.019833333333333335 0.49933333333333335 0.005833333333333334 0.023333333333333334 0.011666666666666667 0.015166666666666667 0.12133333333333333 0.08633333333333333 0.0175 0.004666666666666667 0.014000000000000002 -0.004666666666666667 0.12133333333333333 -0.08633333333333333 0.014000000000000002 -0.018666666666666668 0.004666666666666667 -0.019833333333333335 -0.019833333333333335 -0.4981666666666667c-0.0023333333333333335 -0.011666666666666667 -0.0105 -0.019833333333333335 -0.019833333333333335 -0.021Zm0.3091666666666667 -0.13183333333333336 -0.015166666666666667 0.0023333333333333335 -0.21583333333333335 0.1085 -0.011666666666666667 0.011666666666666667 -0.0035000000000000005 0.012833333333333334 0.021 0.5016666666666667 0.005833333333333334 0.014000000000000002 0.009333333333333334 0.008166666666666668 0.23450000000000004 0.1085c0.014000000000000002 0.004666666666666667 0.026833333333333334 0 0.03383333333333334 -0.009333333333333334l0.004666666666666667 -0.016333333333333335 -0.03966666666666667 -0.7163333333333334c-0.0035000000000000005 -0.014000000000000002 -0.011666666666666667 -0.023333333333333334 -0.023333333333333334 -0.025666666666666667Zm-0.8341666666666667 0.0023333333333333335a0.026833333333333334 0.026833333333334334 0 0 0 -0.0315 0.007000000000000001l-0.007000000000000001 0.016333333333333335 -0.03966666666666667 0.7163333333333334c0 0.014000000000000002 0.008166666666666668 0.023333333333333334 0.019833333333333335 0.028000000000000004l0.0175 -0.0023333333333333335 0.23450000000000004 -0.1085 0.011666666666666667 -0.009333333333333334 0.004666666666666667 -0.012833333333333334 0.019833333333333335 -0.5016666666666667 -0.0035000000000000005 -0.014000000000000002 -0.011666666666666667 -0.011666666666666667 -0.21466666666666667 -0.10733333333333334Z" strokeWidth="1.1667" />
                <path fill="currentColor" d="M14 2.916666666666667A1.75 1.75 0 0 1 15.750000000000002 4.666666666666667v6.302333333333334L21.207666666666668 7.816666666666667a1.75 1.75 0 0 1 1.75 3.031L17.5 14l5.457666666666667 3.151166666666667a1.75 1.75 0 0 1 -1.75 3.031l-5.457666666666667 -3.1500000000000004V23.333333333333336a1.75 1.75 0 0 1 -3.5 0v-6.302333333333334L6.792333333333334 20.183333333333337a1.75 1.75 0 1 1 -1.75 -3.031L10.5 14 5.042333333333334 10.848833333333333a1.75 1.75 0 0 1 1.75 -3.031l5.457666666666667 3.1500000000000004V4.666666666666667A1.75 1.75 0 0 1 14 2.916666666666667Z" strokeWidth="1.1667" />
              </g>
            </svg>
            <p className="cta-callout-title min-w-0 font-semibold text-zinc-800 dark:text-zinc-200">
              {title}
            </p>
          </div> : null}
        <div className={`cta-callout-body text-sm leading-normal text-zinc-800 dark:text-zinc-200${title ? " cta-callout-body--indented" : ""}`}>
          {children}
        </div>
        <a href={href} target={target} rel={linkRel} onClick={handleCtaClick} data-docs-cta={trackingEvent || undefined} className="cta-callout-button inline-flex items-center justify-center gap-1 rounded-full bg-white px-3 py-1.5 text-sm font-semibold transition hover:bg-zinc-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 dark:bg-white dark:hover:bg-zinc-200">
          {buttonLabel}
          <span className="cta-callout-button-arrow" aria-hidden="true">
            →
          </span>
        </a>
      </div>
    </section>;
};

<Note>
  This feature is in private beta. If you want access, please [contact Support](mailto:support@blnkfinance.com?subject=Interested%20in%20Custom%20Apps).
</Note>

You have wired install, launch, and Cloud API calls. Before users rely on your app in production, use this page as a final checklist for security and reliability.

***

## How Blnk Cloud keeps apps safe

Blnk Cloud is designed so your app never needs direct access to the Core. Learn more: [How apps work](/cloud/apps/how-apps-work).

| Blnk Cloud handles                                          | You handle in your app                                |
| ----------------------------------------------------------- | ----------------------------------------------------- |
| A scoped API key for each install                           | Protecting and storing the API key safely             |
| Permission checks on every request                          | Securing the portal users see in the dashboard        |
| Cancelling the install if your app does not confirm in time | Checking granted permissions before sensitive actions |

***

## Protect secrets

The install callback includes a scoped API key. Treat it like any production credential.

* **Encrypt before you save it.** Do not store the key in plain text in your database. Use encryption or a secrets manager that fits your stack.
* **Keep keys on the server.** Never put the scoped API key in the browser, in a `portal_url`, or in client-side code.
* `Confirm installs quickly.` Return a successful response within about `10 seconds` of the install callback. If your app does not confirm in time, Blnk cancels the installation and revokes the key.
* `Log safely.` Use install IDs and key prefixes for debugging. Do not log full API keys, portal session tokens, or third-party secrets.

<Warning>
  If an install fails after you received the key, assume that key is no longer valid. Wait for a successful install event before using a new key.
</Warning>

***

## Install and uninstall events

Blnk sends install and uninstall events to your callback URL. See [Managing installs](/cloud/apps/installation) for payload fields and code examples.

* `Process each event once.` Use the `idempotency_key` Blnk sends to detect duplicates. If you already handled an event, return success again without repeating side effects.
* `On uninstall, stop using that install.` Mark the installation inactive, remove portal sessions, and stop background jobs that used that install's API key.

<Note>
  The install is already tied to the workspace and Cloud instance the user chose in the dashboard.

  You do not need a separate organization field in the callback payload"save `installed_app_id` and `instance_id` so your backend knows which installation it is serving.
</Note>

***

## Permissions

Request only what your app needs in the manifest. Users can approve a subset of what you ask for.

* `At runtime, trust `granted\_permissions`.` That list from the install event is what the user approved. Check it before reads, writes, or alert actions even if your manifest requested more.
* `Your app does not inherit user access.` A workspace admin installing your app does not give the app their personal permissions. The app can only do what was granted to that install.

Supported permission scopes today:

| Scope          | What it allows                               |
| -------------- | -------------------------------------------- |
| `data:read`    | Read ledger data through Cloud.              |
| `data:write`   | Perform write operations through Cloud APIs. |
| `alerts:read`  | Read alerts for the installed instance.      |
| `alerts:write` | Create and update alerts through Cloud.      |

***

## Calling Blnk Cloud from your backend

All Cloud API calls from your app should run on the server using the install's scoped API key.

* Use the Cloud base URL and authentication pattern from [App development](/cloud/apps/app-logic).
* `Always include the correct `instance\_id\`\` for the installation you are acting on. The key is bound to that instance.
* Use the [Proxy API](/cloud/proxy/proxy-api) for Core actions and the [Data API](/cloud/proxy/data-api) for reads and filters. Use alerts endpoints for alert features"not the proxy.

<Tip>
  Your portal UI should call **your** backend. Your backend calls Blnk. That keeps the scoped key off the user's device.
</Tip>

***

## URLs you register in the manifest

When you [register your app](/cloud/apps/register-app), you provide callback and portal URLs that Blnk calls from the cloud.

* **Production:** Use public **HTTPS** endpoints that Blnk can reach from the internet.
* **Local development:** Use a tunnel service (for example ngrok) and register the HTTPS URL. `localhost` will not work when your app is embedded in the Cloud dashboard.
* Blnk validates callback and portal URLs when you register the app. Invalid or unreachable URLs will block registration or launch.

***

## Portal and dashboard embedding

When a user launches your app, Blnk opens your `portal_url` inside the dashboard. This section is the full policy for embedding safely.

See [Launch your app](/cloud/apps/launch-app) for the launch flow.

### Content Security Policy

Set this header on pages Blnk loads in the iframe:

```http Content-Security-Policy wrap theme={"system"}
Content-Security-Policy: frame-ancestors 'self' https://blnkfinance.com https://*.blnkfinance.com
```

Only Blnk-owned domains can embed your portal. Other sites"including `localhost` in production"will be blocked by the browser. `'self'` keeps internal redirects (for example from `/portal/enter` to `/portal`) working inside the frame.

### Portal sessions and URLs

* Return a `new `portal\_url` every time` someone launches the app from Cloud.
* Use **short-lived portal sessions**"roughly 5 to 15 minutes is a good default. Validate the session on every portal page load and API request from the UI.
* The `portal_url` is for your interface only. Do not put API keys, provider secrets, or other sensitive data in the link.

***

## Pre-launch checklist

Before users rely on your app in production, work through this list:

1. API keys are encrypted at rest
2. No secrets in logs, portal URLs, or client-side code
3. Install callback responds within about 10 seconds
4. Handlers dedupe events with `idempotency_key`
5. Uninstall disables the install and stops related jobs
6. Manifest requests only needed scopes
7. Backend checks `granted_permissions` before sensitive actions
8. Every Cloud API call includes the correct `instance_id`
9. Production URLs are public HTTPS endpoints Blnk can reach
10. Local testing uses a tunnel, not raw `localhost` in the manifest
11. `frame-ancestors` policy is set on portal pages
12. Fresh portal URL and short-lived session on each launch
13. Portal talks to your backend; backend talks to Blnk

***

<CtaCallout title="Need help building your app?" href="https://blnkfinance.com/contact/us?utm_source=blnk_docs&utm_medium=documentation&utm_campaign=apps-help" buttonLabel="Get Pro Support" trackingEvent="clicked_pro_support">
  We help you build custom apps for your use case or get help building your own from scratch.
</CtaCallout>
