App Publishing and Embedding#
Every publicly accessible Dify app is backed by a Site record that stores the access token, display customizations, and access-control policy for the published web app. Publishing and embedding spans three layers: the Site data model, the console API endpoints that manage it, and the frontend UI that generates embed snippets.
For related narrower topics, see:
- Chatbot Widget Embedding β
embed.js,window.difyChatbotConfig, and the postMessage protocol - Iframe Embedding Security β
X-Frame-Options, CSP headers, andNEXT_PUBLIC_ALLOW_EMBED
The Site Model#
Defined in api/models/model.py, Site maps 1-to-1 with an App. Key columns :
| Column | Type | Notes |
|---|---|---|
app_id | UUID FK | Links site to its App |
code | String(255) | 16-char opaque access token; indexed with status |
title, description | String / LongText | Displayed on the public page |
icon_type, icon, icon_background | String | Branding |
default_language | String | UI locale for the public page |
chat_color_theme / chat_color_theme_inverted | String / Boolean | Theme accent color |
customize_domain | String | Custom domain (if configured) |
copyright, privacy_policy | String | Footer links |
input_placeholder, custom_disclaimer | String / LongText | Input hints; disclaimer max 512 chars |
customize_token_strategy | Enum | must / allow / not_allow β controls whether end-users must provide their own API key |
prompt_public | Boolean | Whether the system prompt is visible to end-users |
show_workflow_steps | Boolean | Default true β show workflow execution steps |
use_icon_as_answer_icon | Boolean | Default false |
status | Enum | normal / disabled β tracks whether site is active |
code is generated by Site.generate_code(16, session=session), which loops until a globally unique 16-character string is found. The app_base_url property resolves the public URL from APP_WEB_URL or the incoming request root .
The Site.code token is exchanged for a passport JWT at /api/passport. See the JWT Authentication article for details.
Console API Endpoints#
All three endpoints require APP_RELEASE_AND_VERSION RBAC permission.
Enable / disable the site β POST /apps/<app_id>/site-enable
Body: { enable_site: bool }. Delegates to AppService.update_app_site_status; returns the full AppDetail response.
Update site settings β POST /apps/<app_id>/site
Body: AppSiteUpdatePayload with all fields optional; each non-None field is patched onto the Site row in place. Also requires edit permission. Updatable fields: title, icon_type, icon, icon_background, description, default_language, chat_color_theme, chat_color_theme_inverted, customize_domain, copyright, privacy_policy, input_placeholder, custom_disclaimer, customize_token_strategy, prompt_public, show_workflow_steps, use_icon_as_answer_icon .
Reset access token β POST /apps/<app_id>/site/access-token-reset
Generates a new code via Site.generate_code(16). Requires admin or owner (is_admin_or_owner_required) β intentionally stricter than the settings update endpoint.
Frontend Service Layer#
The frontend uses oRPC TanStack Query contracts in web/service/client.ts :
- Site enable/disable β
consoleQuery.apps.byAppId.siteEnable.post.mutationOptions()β invalidates the app detail query ononSettled. - Token reset β
consoleQuery.apps.byAppId.site.accessTokenReset.post.mutationOptions()β invalidates app detail on success. - API enable/disable β
consoleQuery.apps.byAppId.apiEnable.post.mutationOptions()β invalidates app detail on success.
The access-point cards (web-app-card.tsx and service-api-card.tsx) each manage their own pending state. The site settings update endpoint is still called via updateAppSiteConfig and has not yet migrated to the oRPC pattern .
Embedding Options UI#
The Embedded dialog component surfaces three options via OPTION_KEYS = ['iframe', 'scripts', 'chromePlugin'] . Snippet generation lives in app-card-utils.ts:
| Option | Generator | Output |
|---|---|---|
| iframe | getEmbeddedIframeSnippet(iframeUrl) | <iframe src="..."> with allow="microphone;clipboard-write" |
| scripts | getEmbeddedScriptSnippet({url, token, ...}) | window.difyChatbotConfig + <script src=".../embed.min.js"> + CSS overrides |
| chromePlugin | getChromePluginContent(iframeUrl) | Chatbot URL string for the Dify Chatbot Chrome extension |
The iframe URL is assembled by buildEmbeddedIframeUrl as {appBaseUrl}/{webAppRoute}/{accessToken} . Workflow hidden-start variables are gzip+base64-encoded as query parameters via compressAndEncodeBase64. The EmbeddedWebAppRoute type restricts the route segment to 'chatbot' | 'agent', so the dialog works for both app types. Workflow apps with hidden start-node variables expose a collapsible input panel inside the dialog so those values can be baked into the generated snippet .
Service API Access#
In addition to the published web app (Site), each Dify app can be accessed programmatically via the Service API using an API key. This is the preferred integration path for backend systems. The Service API endpoints for chat and completion apps are in api/controllers/service_api/app/completion.py . Service API routes are registered separately from console routes in api/extensions/ext_blueprints.py .
Enabling/disabling API access is controlled separately from site enable/disable via POST /apps/<app_id>/api-enable (tracked via enable_api on the App model).