Skip to main content

Building Databricks Apps for Marketplace

Best practices for ISVs and data providers building closed-source Databricks Apps for distribution via Databricks Marketplace.

1. Authentication and identity

Prefer the App Service Principal over PATs — always

Every Databricks App is automatically provisioned a dedicated service principal (SP). Credentials are injected as DATABRICKS_CLIENT_ID and DATABRICKS_CLIENT_SECRET. Use the SDK with these — never PATs. PATs leak, expire on rotation, and don't carry the per-app identity that the consumer's workspace admin needs to govern.

from databricks.sdk import WorkspaceClient
w = WorkspaceClient() # picks up DATABRICKS_CLIENT_ID / SECRET automatically

Use On-Behalf-Of-User (OBO) for anything touching consumer data

When the app reads or writes the consumer's data, route through the user's identity. Databricks forwards the user token in the x-forwarded-access-token HTTP header. OBO ensures Unity Catalog row filters, column masks, and table ACLs are enforced.

Rule of thumb:

IdentityWhen to use
App SPApp-owned operations: telemetry, internal jobs, calling your own services
OBO (user token)Any access to the consumer's UC data, warehouses, or serving endpoints

Known SDK gotcha when combining OBO with WorkspaceClient

Passing the forwarded user token to WorkspaceClient(token=user_token) will fail with more than one authorization method configured: oauth and pat because the SDK assumes token= means PAT.

Patterns that work:

  • For SQL: pass the token directly to sql.connect(..., access_token=request.headers.get("x-forwarded-access-token"))
  • For SDK calls under OBO: construct a fresh client with the OAuth token explicitly using Config(host=..., token=user_token, auth_type="oauth-m2m") or use databricks.sdk.config.Config with credential strategy override

Test against the docs example before shipping.

Declare the minimum scopes

Default scopes are intentionally narrow (iam.access-control:read, iam.current-user:read). Request only what the app actually needs. Consumers' security reviews flag broad scopes, and install conversion drops every time you add one.

Multi-tenancy and user isolation

For OBO apps, implement proper user isolation. Two users hitting the same app instance must not see each other's data. Don't cache OBO-scoped query results in process-global state.

Never log or persist tokens

Ensure that tokens are not printed, logged, or written to files. Strip auth headers from any error reporting or trace export.

Audit-log OBO actions

For every action you take on behalf of a user, record a structured log line: user identity, action, target resource, status. Consumers' compliance teams will ask.


2. Resources and configuration

Declare everything in app.yaml

Your app declares resources (warehouses, secrets, serving endpoints, jobs, Genie spaces, UC tables/volumes) so the consumer's admin can review and bind them at install time. Hardcoding workspace IDs, warehouse IDs, or table paths breaks portability across consumer workspaces.

command: ['streamlit', 'run', 'app.py']
env:
- name: DATABRICKS_WAREHOUSE_ID
valueFrom: sql_warehouse # bound by consumer admin at install
- name: API_KEY
valueFrom: my-secret # bound to a consumer-managed secret

Use valueFrom, not value, for any secret. value is plain text in your bundle.

Use the consumer's secret scope, not yours

Any API keys, license keys, or third-party credentials your app depends on should bind to a consumer-owned secret scope at install time — not be baked in from your dev environment. Declare a secret resource in app.yaml; the consumer's admin populates it with their own credentials at install.

This ensures:

  • The consumer manages rotation and revocation on their own schedule
  • You never see or store their keys
  • Each consumer uses their own provider account (Stripe, OpenAI, etc.) — important for billing isolation and quota separation

Document in your install instructions exactly which secrets the consumer must provide and where to obtain them.

Don't share an SP across apps

Each app gets its own SP and that SP cannot be reused. The per-app SP is the unit of governance the consumer operates on.

Apply least privilege per resource

Grant the app SP only the permissions the app needs on the bound resource (e.g., CAN_USE on a warehouse, not CAN_MANAGE). Document what permissions the consumer needs to grant in your install instructions.


3. Build on Databricks-native assets

Your app will run, scale, and govern best when its dependencies stay inside the Databricks ecosystem. Reach for native services first; treat external dependencies as the exception.

Instead ofUse
External PostgresLakebase — same operational model, governed by Unity Catalog, no extra egress
External model providerModel Serving and Foundation Model APIs — inference stays inside the consumer's workspace, governed by UC, billed via DBUs
External vector DBVector Search for embeddings
Custom compute infrastructureDatabricks Jobs and DLT for background pipelines
External warehouse or object storeDelta tables in Unity Catalog for any analytical outputs

Every Databricks-native dependency is one fewer external domain to declare for SEG, one fewer credential to manage, one fewer security review with the consumer's team — and your app's value stays entirely inside the lakehouse the customer is already paying for.


4. Networking and external access

Declare every external domain precisely

If your app talks to api.acme.com, cdn.acme.com, or any package registry at runtime, declare those domains in your Marketplace listing's required-egress metadata.

  • Consumer workspaces with Serverless Egress Gateway (SEG) policies will block undeclared traffic with no graceful fallback
  • The narrower and more reputable your declared domains, the higher your adoption in security-sensitive customers (financial services, healthcare, public sector)

Minimum domains for any app to deploy: *.databricksapps.com, pypi.org, files.pythonhosted.org, registry.npmjs.org (package registries not applicable for scalable apps). AWS deployments often need *.amazonaws.com for S3 / STS.

Understand SEG governance at install time

SEG is a workspace-level setting controlled by account admins — not workspace admins, and not the user installing your app.

At install, Databricks performs a pre-check against the consumer's SEG policy and surfaces the specific domains the app needs that are not allowed. The consumer must contact their account admin out-of-band to update SEG.

Test against a SEG-restricted workspace before shipping

The most common Marketplace install failure mode: app installs successfully but errors on first request because a runtime domain wasn't allowlisted. Reproduce a restricted-egress workspace in QA, run your app's full happy path, and check system.access.outbound_network for any denied connections you missed.

Don't assume internet at runtime

Pre-fetch what you can at build/deploy time. Avoid runtime calls to pip install, npm install, or remote model downloads — they will fail in restricted workspaces and they slow cold starts everywhere.

Plan for runtime-dynamic dependencies (agentic apps)

If your app is agentic and the set of external destinations depends on user prompts (e.g., "talk to my Slack"), pre-declaration is impossible. The platform direction is that agentic apps run in isolated containers with internet access off by default, where the developer or user explicitly allowlists per-app domains, intersected (AND) with the workspace SEG policy.

Design for runtime failures with actionable error messages: surface the specific domain that failed and tell the user to contact their account admin.


5. Source code and updates

Repository standards

  • Ship human-readable source code only — no pre-compiled binaries, packaged executables, obfuscated code, or other opaque artifacts
  • Host the app in a repository you control with branch protections enabled
  • Pin every Marketplace publication to a specific git tag or commit hash, not a moving branch like main
  • Include a SECURITY.md with vulnerability reporting procedures
  • Include a dependency manifest (requirements.txt, pyproject.toml + lockfile, or package.json + lockfile) so consumers' security teams can scan

Code review

  • Two-person approval for any code change going to Marketplace
  • Static analysis / linting in CI; block merge on failures

Update lifecycle

  • Communicate breaking changes via a deprecation notice flow consumers can subscribe to
  • Decide your update cadence and document it in your listing

6. Deployment

Deploy your app as a git-based app. It keeps version management simple and lets you sync git tags and versions with your Marketplace updates.

Use git-based deployment, not manual upload

Configure the app against a git repository (GitHub / GitLab / Bitbucket). Each databricks apps deploy reads from a specific branch, tag, or commit, which means your deploy is reproducible and reviewable.

databricks apps create my-app \
--json '{"git_repository":{"url":"https://github.com/acme/forecast","provider":"gitHub"}}'

databricks apps deploy my-app \
--json '{"git_source":{"branch":"main"}}'

Manual workspace folder uploads (databricks sync + deploy from path) work, but they don't give you the audit trail or rollback story you'll want when a consumer reports a regression.

Use Databricks Asset Bundles (DABs) for environment promotion

Define your app in databricks.yml with targets for dev, staging, and prod. Promote between environments by changing the --target flag, not by editing config. Bundles are also how you wire CI/CD on every push.

Pin dependencies

Use requirements.txt with exact pins (or uv.lock / pyproject.toml with a lockfile). A floating dependency is a release that breaks on whatever day PyPI changes upstream — and you won't know which consumer hit it.

Tag every release

Git tag every release that goes to Marketplace. When a consumer files a bug, "what tag are you on" is the first question.


7. Local development and testing

Validate locally before submitting

Use databricks apps run-local to run your app outside the Databricks runtime against a local config. Validate your app.yaml, env injection, and SDK calls before you push to a workspace.

Reproduce the consumer install in a clean workspace

Before submitting to Marketplace QA, install your own app in a workspace with no preexisting setup. Walk through the install path the way a consumer admin would. Most install bugs are caught here — missing permission grants, hardcoded paths, silent assumptions about preexisting catalogs.

Test the OBO and App-SP paths separately

A common mistake: every dev test runs as you (full admin), so OBO failures only surface when a low-privilege consumer user tries the app. Test with a dedicated low-privilege user account.

Test against a SEG-restricted workspace

See Networking. This is the most important pre-submission test for Marketplace.


8. Observability and operations

Send structured logs to UC

Configure app telemetry to write traces, logs, and metrics to Unity Catalog. Consumers' platform teams want to see what your app is doing in their environment without filing a ticket with you.

Expose health-check endpoints

The Apps REST API exposes app_status.state and compute_status.state (GET /api/.../apps/{name}). Document for consumers how to wire that into their monitoring (PagerDuty, Datadog) and what your "healthy" state looks like.

Make errors actionable for the consumer admin

When the app errors, surface a message the consumer's admin can act on: "Required domain api.acme.com is blocked by SEG — contact your account admin." Don't dump a stack trace.


9. App telemetry and custom instrumentation

You instrument your app so that, once a consumer installs it, it captures usage, activity, and error telemetry via OpenTelemetry (OTel) into the consumer's Unity Catalog otel_* tables. Those tables reach you through the telemetry shareback flow, which the installer explicitly enables.

System logs populate automatically. Everything else — time-to-value, feature adoption, where users get stuck — only exists if you emit it.

What you own vs. what the installer enables

You (provider) buildInstaller (consumer) enables
Instrumentation in the app source: auto and custom spans, metrics, and attributes; deciding what is captured; keeping it free of sensitive dataTelemetry export to a UC schema, and shareback of the otel_* tables to the provider

Turn on auto-instrumentation

Wrap the app command with opentelemetry-instrument and add the OTel packages. This yields request traces and a logging bridge with zero app logic. Databricks auto-configures the OTLP endpoint and resource attributes — never hardcode them.

Frameworkapp.yaml command
Flask['opentelemetry-instrument','gunicorn','app:app','-b','0.0.0.0:$DATABRICKS_APP_PORT']
FastAPI['opentelemetry-instrument','uvicorn','app:app','--host','0.0.0.0','--port','$DATABRICKS_APP_PORT']
Streamlit['opentelemetry-instrument','streamlit','run','app.py']
Dash['opentelemetry-instrument','python','app.py']
Node.jsCreate an otel.js init file and require it at startup
requirements.txt
opentelemetry-distro
opentelemetry-exporter-otlp-proto-grpc
opentelemetry-instrumentation-flask # match your framework
app.yaml
env:
- name: OTEL_TRACES_SAMPLER
value: 'always_on' # for normal Databricks Apps browser traffic

Add custom instrumentation

Auto-instrumentation only sees HTTP requests. The signals providers actually want come from spans, metrics, and events you emit yourself. At a minimum, instrument:

  • Onboarding / activation funnel — one span per setup step, a duration metric, and an outcome attribute. This measures time-to-value and pinpoints drop-off.
  • Feature usage — a counter per meaningful action or feature.
  • Errors and roadblocks — let the exception propagate out of the span (the span context manager records it and sets ERROR status automatically) and increment an error counter tagged with a reason.
  • External dependency calls — wrap calls to your own APIs and model serving endpoints in spans to see latency and failures.
  • Activity — a session-start / active-user signal, keyed by an opaque install identifier.
import time
from opentelemetry import metrics, trace

tracer = trace.get_tracer("marketplace-app")
meter = metrics.get_meter("marketplace-app")

step_ms = meter.create_histogram("setup.step.duration_ms", unit="ms")
feature = meter.create_counter("feature.invocations")
errors = meter.create_counter("app.errors")

step = "connect_data"
with tracer.start_as_current_span(f"setup.{step}") as span:
span.set_attribute("setup.step", step)
span.set_attribute("app.version", APP_VERSION)
span.set_attribute("install.id", INSTALL_ID)
started = time.perf_counter()
outcome = "error"
try:
do_work()
feature.add(1, {"feature": step})
outcome = "ok"
except Exception as exc:
errors.add(1, {"step": step, "reason": type(exc).__name__})
# Let the exception leave the span context. The context manager
# records it and sets ERROR status under its default settings.
raise
finally:
elapsed_ms = (time.perf_counter() - started) * 1000
span.set_attribute("setup.outcome", outcome)
step_ms.record(elapsed_ms, {"step": step, "outcome": outcome})
Spans that succeed report UNSET

A span that completes successfully normally retains OpenTelemetry status UNSET, which can appear as NULL in otel_spans. If your code catches an exception and does not re-raise it, call span.record_exception(exc) and span.set_status(ERROR) explicitly — otherwise the failure won't be visible in shareback.

Info-level logs are dropped by default

Bridge your normal app logs into the same pipeline by adding opentelemetry-instrumentation-logging>=0.61b0 to requirements.txt, so logger.info(...) lands in otel_logs alongside the traces. Call logging.basicConfig(level=logging.INFO) at startup first — the Apps runtime leaves the root logger at WARNING, so info-level logs are otherwise dropped silently.

Keep the attribute schema stable

Tag every signal with a stable set of keys — app.version, install.id, feature, setup.step, outcome. Keep these keys stable across releases: your shareback analytics query these columns, so renaming an attribute silently breaks your dashboards.

Correlate without sensitive customer data

Use an opaque, app-generated install/tenant identifier to group activity per consumer — not emails, usernames, or workspace identifiers that resolve to a person. This lets you segment by installer after shareback without carrying sensitive data.

Rule of thumb: capture the app's behavior, not its payloads. This telemetry is designed to leave the consumer's workspace via shareback, so never emit tokens, secrets, PII, or the consumer's business records into otel_*. Instrument step names, durations, versions, feature names, and outcomes only.

Test telemetry locally before publishing

Run the app under opentelemetry-instrument with OTEL_TRACES_SAMPLER=always_on and confirm your custom spans and metrics appear (query otel_spans / otel_metrics) before you list a new version. Sample traces in production.


10. Performance and cost

Design for scale-down

App compute is billed per provisioned hour. Configure to scale down between requests if traffic is bursty; don't keep large models or warehouse connections hot when the app is idle.

Manage cold starts

Avoid heavy imports and model downloads at startup. Lazy-load anything that isn't needed on the first request path. Cold start is the consumer's first impression.

Cache provider-side state outside the app

If your app calls your own backend, cache there, not inside the per-consumer app instance. App instances are scoped per workspace; they're a bad place to hold state you'd rather have global.


Marketplace readiness checklist

Before submitting your app for QA:

  • Built on a supported framework: Python (Streamlit / Dash / Gradio) or Node.js (React / Angular / Svelte / Express)
  • Created a readme.md to attach with your app listing
  • No PATs anywhere in code or config
  • App SP and OBO usage matches the rule of thumb
  • All scopes are minimal and documented
  • All external domains declared precisely in listing metadata — no "whole internet" entries unless you've explicitly accepted the TAM tradeoff
  • Verified against a SEG-restricted workspace; install path surfaces actionable messages for blocked domains
  • All resources (warehouses, secrets, etc.) declared in app.yaml, none hardcoded
  • Secrets bind to consumer-owned scopes at install — no API or license keys baked into the bundle
  • Native Databricks services preferred over external equivalents (Lakebase, Model Serving, Vector Search) where possible; external dependencies justified
  • Dependencies pinned via lockfile
  • SECURITY.md published; vulnerability reporting path documented
  • Git-based deploy configured; all Marketplace releases pinned to a specific tag/commit
  • Audit logs structured, no tokens in any log path
  • Auto-instrumentation enabled (opentelemetry-instrument plus OTel dependencies in the manifest)
  • Custom spans and metrics emitted for setup steps, feature usage, and errors — time-to-value and drop-off are measurable
  • Stable telemetry attribute schema with app.version and an opaque, non-PII install.id
  • No tokens, secrets, PII, or consumer data in otel_*; traces sampled in production
  • Cold start under target threshold (recommend < 10s to first request)
  • Health check / status endpoint documented for consumer monitoring teams
  • Consumer install flow walked end-to-end in a clean workspace
  • Real, functional product — not a POC, demo, or placeholder

References