App Publishing and Embedding#
Every Dify app that is made publicly accessible is backed by a Site record. The Site stores the access token (code), display customizations, and access-control policy that governs how users reach the published web app. This article covers the data model, the API endpoints that manage it, and the frontend that generates embed snippets.
The Site Model#
Defined in api/models/model.py, the Site table 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 |
The code column is set 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 .
API Endpoints#
Enable / Disable the site#
POST /apps/<app_id>/site-enable
Takes { enable_site: bool }. Requires APP_RELEASE_AND_VERSION RBAC permission. Delegates to AppService.update_app_site_status. Returns the full AppDetail response.
Update site settings#
POST /apps/<app_id>/site
Accepts an AppSiteUpdatePayload with all customizable fields as optional. Each non-None field is patched onto the Site row in place. Requires edit permission + APP_RELEASE_AND_VERSION RBAC.
Updatable fields via this endpoint :
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) plus APP_RELEASE_AND_VERSION RBAC — intentionally stricter than the settings update endpoint.
Frontend service layer#
The frontend invokes these endpoints through oRPC TanStack Query contracts defined in web/service/client.ts:
-
Site enable/disable —
consoleQuery.apps.byAppId.siteEnable.post.mutationOptions()takes{ params: { app_id: string }, body: { enable_site: boolean } }. Components calluseMutationwith these options; the mutation automatically invalidates the app detail query on success via itsonSettledcallback. -
Site access token reset —
consoleQuery.apps.byAppId.site.accessTokenReset.post.mutationOptions()takes{ params: { app_id: string } }. The mutation invalidates the app detail query on success; components supply their ownonSuccesscallback to coexist with the shared invalidation logic. -
API enable/disable —
consoleQuery.apps.byAppId.apiEnable.post.mutationOptions()takes{ params: { app_id: string }, body: { enable_api: boolean } }. The mutation invalidates the app detail query on success.
Mutation ownership: the individual access-point cards (web-app-card.tsx, service-api-card.tsx) manage their own pending state via useMutation, while shared invalidation is configured in the mutation defaults in client.ts.
The settings update endpoint is still wrapped by updateAppSiteConfig in web/service/apps.ts; that operation has not yet migrated to the oRPC contract pattern.
Embedding Options UI#
The Embedded component in web/app/components/app/overview/embedded/index.tsx is a Dialog that the app overview page opens. It renders three embed options via the OPTION_KEYS = ['iframe', 'scripts', 'chromePlugin'] constant .
All snippet generation lives in web/app/components/app/overview/app-card-utils.ts:
| Option | Generator | Output |
|---|---|---|
| iframe | getEmbeddedIframeSnippet(iframeUrl) | Plain <iframe src="..."> tag with allow="microphone;clipboard-write" |
| scripts | getEmbeddedScriptSnippet({url, token, ...}) | window.difyChatbotConfig object + <script src=".../embed.min.js"> + CSS override block |
| chromePlugin | getChromePluginContent(iframeUrl) | Just the chatbot URL string for the Dify Chatbot Chrome extension |
The iframe URL is built by buildEmbeddedIframeUrl : it assembles {appBaseUrl}/{webAppRoute}/{accessToken} and encodes any workflow hidden-start variables as gzip+base64 query parameters via compressAndEncodeBase64 .
The EmbeddedWebAppRoute type constrains the route segment to 'chatbot' | 'agent' , so the embed dialog works for both chatbot and agent-type apps. 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 .
Related Articles#
- Chatbot Widget Embedding — deep-dive into
embed.js,window.difyChatbotConfig, and postMessage protocol - Iframe Embedding Security —
X-Frame-Options, CSP, andNEXT_PUBLIC_ALLOW_EMBED - JWT Authentication — how the
Site.code(access token) is exchanged for a passport JWT at/api/passport