Editions and surface profiles
Compare the Community, Business and Cloud editions, switch a self-hosted Nodaro to Business, and narrow its interface with a NODARO_SURFACE_PROFILE.
Nodaro has three editions — Community, Business and Cloud — built from the same code and selected with the EDITION variable. On the Business edition, a surface profile then narrows what the install shows, without a rebuild. It can hide parts of the interface, remove nodes and models, rename the product and restrict sign-in. This page covers both.
The three editions
| Community | Business | Cloud | |
|---|---|---|---|
| Self-hostable | Yes | Yes | No, managed by Nodaro |
| Admin panel | No | Yes | Yes |
| User management | No | Yes | Yes |
| Credit ledger | No | No | Yes |
| Billing | No | No | Yes |
| Credit prices set by admins | No | No | Yes |
| Surface profile | Ignored | Yes | Yes |
- Community (
EDITION=community, the default) is the free self-hosted edition. Anyone who signs up becomes a regular user. - Business (
EDITION=business) adds the admin panel and user management. It is still self-hosted and still has no billing. - Cloud (
EDITION=cloud) adds credits and billing. It powers app.nodaro.ai and is not meant for self-hosting.
The Business edition's admin panel and user management are Enterprise features. You may run them for development and testing, but using them in production requires a Nodaro Enterprise subscription. See License.
Switch a self-hosted install to Business
The API reads EDITION at startup. The editor in the browser reads its edition from VITE_EDITION when the image is built. The published image is built as the Community Edition, so a Business install is built from source.
Moving from Community to Business has no database cost: the schema is the same.
Change both values in the compose file
In docker-compose.community.yml, under the nodaro service, change the two lines that say community:
nodaro:
build:
args:
VITE_EDITION: business # was: community
environment:
EDITION: business # was: communityBoth lines are fixed in the compose file, so setting EDITION in .env has no effect.
Build and start
docker compose -f docker-compose.community.yml build --no-cache nodaro
docker compose -f docker-compose.community.yml up -dUpdate a source-built install with git pull and build. docker compose pull would download the published Community image again.
The build refuses an empty or unknown VITE_EDITION. Without that check, an unset value would build cleanly and fall back to community, and a Business image would ship without its admin panel. The compose file passes the value for you. A hand-written docker build needs --build-arg VITE_EDITION=community, business or cloud.
What is fixed at build time and what is not
Three browser settings are not fixed at build time: the API address, the browser's sign-in address and the anon key. At boot, the container writes them into /config.js from PUBLIC_URL, FRONTEND_SUPABASE_URL and SUPABASE_ANON_KEY, and the browser reads that file before the app starts. So the published image serves any port or domain after a restart. The VITE_* build arguments are only fallbacks for unset runtime values.
Other build-time browser settings:
| Build argument | What it does | Default |
|---|---|---|
VITE_STUDIO_URL | The address of the Studio app, for Open in Studio links | https://studio.nodaro.ai |
VITE_PERSON_URL | The address of the Person app, for the Open Person card on the home screen | https://person.nodaro.ai |
Surface profiles
NODARO_SURFACE_PROFILE narrows the interface of a Business or Cloud install without a rebuild. The Community Edition ignores it and always shows the full interface.
- Format. Inline JSON, or
@/path/to/profile.jsonto read a file. To use a file on the compose stack, mount it into the container and give its path inside the container. - Narrow only. A profile can hide and remove. It never enables something that the edition disables.
- Every field is optional. An empty list means "keep the default". Unset, the install shows the full interface.
- Errors. A malformed field falls back to that field's default, with a warning in the log. A profile that does not load at all stops a Business or Cloud install from booting, with
[surface-profile] FATAL … Refusing to boot a narrowing deployment mainline-open. That happens with an unreadable file, invalid JSON, or a value that fails validation as a whole. A narrowing install must never come up showing everything. - Apply. Restart the app. The profile reaches the browser through
/config.js.
Example
NODARO_SURFACE_PROFILE={"nav":{"hide":["gallery"]},"brand":{"productName":"Studio"},"outputs":{"allowPublic":false},"voice":{"allowedGenders":["male"]}}This hides the gallery, renames the product to Studio, keeps every output private and offers male voices only.
Fields
| Field | What it does |
|---|---|
nav.hide | Hides sidebar entries. Values: gallery, explore, pricing, templates, apps, community, integrations. |
dashboard.tabs | One ordered whitelist of the home screen's sections. See Home-screen sections. |
nodes.deny, models.deny | Node types and model IDs to remove everywhere: the node picker, GET /v1/nodes, GET /v1/models, the MCP tools, and at run time. A removed node fails with node_not_available. |
nodes.allow, models.allow | Whitelists. When a list is not empty, only the listed node types or model IDs are offered, and deny still subtracts from them. See Allow lists. |
auth.methods, auth.ssoLabel | The sign-in methods to offer: email, google, sso. sso is dropped unless auth.ssoLabel is set. See Sign-in methods. |
siblings.apps | Replaces the Nodaro links in the product switcher: [{ "label": "...", "url": "..." }]. |
brand.productName | Replaces the wordmark and the page title. Absent, the page title stays as shipped. See The brand in the page head. |
brand.wordmark | Short text shown beside your own logo in the sidebar header, for an install that ships its own logo files. For example, product name Acme Studio with wordmark Studio. |
brand.description | Replaces the page's meta description. |
brand.favicon | The browser icon: one path on your site, or a list of up to four. See The brand in the page head. |
brand.platformLinks | false hides the platform's own legal, documentation, release-note and product-promotion links. Links you set in siblings.apps stay. |
locale.default | The language a new visitor starts in. |
locale.picker | false hides the language picker. The default is true, also when you leave the key out beside the other locale keys. |
locale.offered | The languages a visitor may choose from, as locale IDs such as ["he","en"]. See Offered languages. |
outputs.allowPublic | false makes every output private, whatever the user chooses, and closes every read that hands one person's work to someone else. See Private outputs. |
jobs.errorDetail | Who reads the provider's own words about a failed job: "owner" (the default) or "admin". See Provider words. |
imports.links | false refuses media imports from a link to another site. See Link imports. |
strings | Your own wording for the editor's interface strings, per language. See Your own wording. |
voice.allowedGenders | Restricts voices to any subset of male, female and neutral. Empty means every gender. See Voice genders. |
features.hide | Switches whole features off: copilot removes the Workflow Copilot, and presentation hides the canvas Present tab. |
catalogs | { "required": true, "factoryPresets": false } for installs that need reviewed picker content, and images for the pictures on picker options. See Picker catalogs. |
mcp.byPath, mcp.path | {"mcp":{"byPath":true}} serves the MCP endpoint under a path on the install's own hosts, /v1/mcp by default, instead of on a dedicated MCP host. See MCP by path. |
The profile does not carry image files. Ship your logo and icon files with a layer of static files in your Docker image. brand.favicon then names the icons in the page head.
Home-screen sections
dashboard.tabs is one ordered whitelist. The full key set is workflows, projects, apps, miniapps, templates, tutorials, statistics, gallery, studio and mcp.
workflows,projects,studio— the lists of the Continue tab's workspace filter, in the order of your list. If your list names none of the three, all three show, so the main list is never blank.apps— the Nodaro apps band on Continue.templates— the "Start from a template" row on Explore, and the Templates sidebar entry.tutorials— the Level up tutorials on Explore, and the Tutorials sidebar entry. Withouttemplatesandtutorials, the Explore tab is not shown.miniapps— the MiniApps sidebar entry.statistics— the statistics overview on the Executions page.mcp— the fourth "Jump back in" list: the workflows an MCP client created in its automaticmcpproject.
List every section you want to keep. For example, ["workflows","projects","statistics","tutorials"] keeps the workflow and project lists, the tutorials and the statistics. It drops the Studio list, the apps band, the template row and the Templates and MiniApps sidebar entries. nav.hide still applies on top.
Allow lists
An allow list is the safer shape for a curated install. A new node or model then stays unavailable until you list it, instead of appearing because you did not deny it.
- Utility nodes, such as
sticky-noteandpreview, are never removed by omission from an allow list. Only an explicitdenyremoves them. - Admins can change node and model availability at runtime in the admin panel. A saved runtime setting replaces the profile's lists until it is reset to the factory state.
- A node that an admin disables in the admin panel stays usable by admins. A node removed by the profile is removed for admins too. But an admin who enables a profile-removed node again in the admin panel releases it to everyone, so give the admin role only to people you trust with that.
- A disabled model is disabled for admins too.
- A run is checked as the user it runs as. An app or presentation run executes as the person running it. A scheduled or webhook-triggered run executes as the workflow's owner, and an API token or a connected app acts as its own owner. When that owner is an admin, the run can use a node hidden from users, so treat the webhook URL of such a workflow with care.
- An app, a component or a template that contains a hidden node cannot be published: the request fails with
node_not_available. - A change of role reaches these checks within about 5 minutes.
A model list judges the model that a request actually runs, not only the model it names:
- Defaults count. A request or a workflow node that names no model is judged on the default it would run, such as the image default for its aspect ratio or the default LLM of a text feature. A model named in another field, such as
llmModel, is judged too. A refused request fails with403 model_not_available, on the run and onPOST /v1/quote, before anything is created or charged. - List every model the install runs. Include the LLMs behind the text features. A request whose model is not listed is refused. It never falls back to an unlisted default.
- New nodes start on an allowed model. In the editor, a new node, or a node reset to its defaults, whose default model is withheld starts on the first model its own model menu offers. A saved, copied or pasted node keeps its model, and a run refuses that model when it is withheld.
- Nodes the server creates. The same rule covers a workflow written by an MCP client, the nodes a Copilot edit adds, a workflow made from a template and the starter workflow of a new account. A model that the writer names is kept. The MCP tools and the Copilot say which nodes started on another model. A workflow written through the REST API is stored as sent.
- Menus with their own values. Some nodes list versions or engines instead of model IDs, such as the Suno versions, the voice changers, Upscale Video, Transcribe and Generate Music. These nodes keep their defaults, so list those defaults, or the nodes are refused at run time.
- No model list, no change. An install that restricts no models keeps every default as it is.
A model list judges the models GET /v1/models lists, and the LLMs. A model that is not in that catalog is not withheld by leaving it out: withhold its node with nodes.allow or nodes.deny instead. These models are in the catalog, so an allow list that leaves one out refuses it, while the node's other models keep running:
- FLUX Fill Pro (
flux-fill), a Modify Image and Image to Image model. - Wan Animate Move and Replace (
wan-animate-move,wan-animate-replace), Motion Transfer models. - A trained character's model (
flux-lora-character). No menu,GET /v1/modelsor MCPlist_modelsoffers it. A Generate Image run switches to it on its own when the one character it names has a trained model. Where a restriction withholds it, the run does not switch. It runs the model the node or request names, at that model's price, and uses the character's reference pictures. A request that namesflux-lora-characteritself is refused with403 model_not_available.
These four models joined the catalog later. A models setting saved in the admin panel before then leaves them off until an admin turns them on.
The lists also hold on the direct API. Every route that runs a node is checked as that node, whatever its path: for example, POST /v1/suno/generate as suno-generate, and a node's /continue and streaming routes as the node itself. With nodes.allow set, a generation route that is linked to no node is refused with node_not_available. The one route that runs no node, the Workflow Copilot, is not checked this way. features.hide controls it.
Three availability ids have no canvas node. List them in nodes.allow and nodes.deny exactly like a node:
cast-swapis Nodaro Motion:POST /v1/cast-swapand its routes, and every trend run.POST /v1/trends/:slug/estimateand/runsanswernode_not_availablewhere it is not offered.film-pipelineis the film pipeline (Story → Video, Nodaro Cloud). Where it is not offered,POST /v1/pipelinesand every pipeline route that starts or continues work answer403 node_not_available, before anything is reserved. Reading a pipeline, switching a run to manual andPOST /v1/pipelines/:id/cancelstay open, so a pipeline started before can still be followed and stopped. A running pipeline does not start its next stage: it fails withnode_not_available, and its unspent reservation is refunded. A pipeline paused at an approval gate stays paused. The MCP toolsstart_pipeline,branch_pipeline,chat_pipeline_stageandapply_chat_proposalare not listed. The canvas node that opens a pipeline,generative-pipeline, is a node of its own, so list both for the editor's pipeline. See Pipelines.video-directoris the Video Director:POST /v1/video-director/run, and the MCP toolscreate_explainerandcreate_launch_video, which are not listed where it is not offered. Its speech, alignment and render steps are each checked as their own node, so also listtext-to-speech,forced-alignmentandrender-video.
A runtime availability setting saved before an id became gateable does not decide that id. cast-swap, film-pipeline and video-director keep their factory state until the setting is saved again, so an upgrade never turns off something users had.
Sign-in methods
auth.methods narrows the sign-in methods on the login page, among email, google and sso. Keep sso in the list only together with auth.ssoLabel, the text of the SSO button.
A list that names only sso also enables a server-side rule: every signed-in account must have been created or linked through single sign-on. Any other session is refused on its first API call with 403 sso_required, so an account registered directly on the sign-in service cannot use the install. Adding email to the list disables the rule. See Single sign-on.
Voice genders
voice.allowedGenders is enforced on the server. An empty list allows every gender.
- Lists. The voice list (
GET /v1/voices), the shared voice library (GET /v1/voices/library) and the voice clones list (GET /v1/voice-clones) show only voices whose gender is known and allowed. The library's filter counts (GET /v1/voices/library/facets) count the same narrowed set. - Use. A voice is judged by its ID alone. Its gender comes from the built-in voice catalog, from the user's own clone, or, for a library voice, from the gender the library stated when this install listed it while the restriction was set. A voice of another gender is refused with
voice_not_available. So is a voice whose gender none of these can establish, such as a library voice picked before the restriction was set. The request is refused before any job is created. - Where it applies. Text to Speech, Text to Dialogue and Voice Changer, on their routes and in workflow runs. Voice Changer Pro on an install connected to Nodaro Cloud. Generate Video with character voices: every voice the clip speaks in. The speech worker checks each voice again before it sends it, so a queued job whose voice is refused fails.
- Dubbing. Dubbing is refused while a restriction is set, on
POST /v1/dubbingand on the Dubbing node. A dub speaks in clones of its source's speakers, or in voices that the dubbing service chooses, so its voices cannot be checked. - Defaults. Every default and fallback voice becomes the first voice of an allowed gender, also when a request names no voice. The editor hides the vocal-gender tags of the other genders in the Suno nodes.
The output gender of the voice-creation nodes cannot be known in advance. To remove them and the Dubbing node, add "nodes":{"deny":["voice-design","voice-remix","dubbing"]}.
The restriction judges the voices that Nodaro chooses. It does not judge:
- An audio track that a request provides. Speech to Video and Lip Sync use it as given.
- Speech that a video model generates in its own voice.
- The voices of the AI Avatar node in its text mode, which come from that node's own voice catalog.
Picker catalogs
For installs that must offer only reviewed picker content, set "catalogs":{"required":true,"factoryPresets":false}.
- With
required: true, the editor offers picker choices only after it receives the complete curated catalog from the server, and offers none if that request fails. New nodes and factory resets use only offered values. - With
factoryPresets: false, the bundled presets are hidden in the editor and the API. Users' own presets stay. - The server also rejects excluded catalog choices in imported or previously saved workflows.
catalogs.images sets the pictures on picker options: "default" (the platform's pictures, the default), "none" or "own". Any other value reads as "none", with a warning. With any value except "default":
- The API leaves every picture out of the picker catalogs. No option carries
imageUrlorimageSrcSet, no Person or Styling topic carriesimageUrl, and everyimageCountin the directory is0. This coversGET /v1/picker-catalogs,GET /v1/picker-catalogs/:nodeType,GET /v1/catalogsand the MCP toolget_picker_catalog. Everything else in those replies is unchanged. See Picker catalogs. - The editor shows none of the platform's pictures. That covers the rendered previews from the Nodaro CDN, and the photos, emoji and flags that the install serves itself. Each picker, the canvas nodes and the app input cards show what they show for an option with no picture: a drawn preview, an icon or the label. The Person card on the home screen shows no photos.
"own" shows the pictures the deployment registered in their place. With none registered, it is exactly "none". With "none", registered pictures are not shown either, and with "default" they are ignored.
- Registering pictures. A deployment registers its own pictures in its own code at startup, in the same place it registers preset packs. Each entry names a catalog id, an option of that catalog, a picture address and an optional larger picture.
- The media host. Every address must be an
httpsaddress on the install's own media host. An address on any other host, the Nodaro CDN included, is refused, and so is a video file. - Checked at boot. A malformed entry, an option registered twice, or an option that is not in the deployment's catalogs stops the boot, with every problem in one message.
- A missing picture is not an error. An option with no picture shows none. The boot log says how many options have a picture, per catalog.
- Where they show. The API's
imageUrlis the picture, and the REST replies also carry the larger one asimageLargeUrl. The MCP tool leaves the larger one out. In the editor, the pictures replace the platform's wherever those would be: in the pickers, on the canvas nodes, on the app input cards and on the Person card. A picker that draws no platform picture draws none of the deployment's either. A Person or Styling topic is not an option, so it has no picture.
The platform's picture files themselves are still served: a file under /picker-art/, or a render on the Nodaro CDN, still loads for anyone who asks for its address. The API and the editor only stop naming them.
Offered languages
locale.offered lists the languages a visitor may choose from, as locale IDs: ["he","en"]. Nodaro reads them in its own spelling, so pt-br becomes pt-BR. Every language list in the editor then offers only these: the sidebar's language switcher, and the language setting in a node's settings panel. A language already on screen stays listed, so that the user can switch away from it.
- An unknown ID is dropped with a warning. A list with no known ID left is ignored, and every language stays offered.
- A
locale.defaultthat the list does not include is reported at boot, with a warning.
Private outputs
With "outputs":{"allowPublic":false}, every output is private, whatever the user chooses. Every read that hands one person's work to someone else also answers as if nothing were public:
- Listings list nothing. That is the public view of
GET /v1/gallery, the community listings (browse and favorites), the app and component marketplace (GET /v1/apps/browse) and the template marketplace (GET /v1/templates/browse). Both marketplaces answer{ data: [], nextCursor: null }, their favorites views included. It is also the public scope of the MCP toolsbrowse_gallery,list_jobs,list_appsandlist_components. - Pages of that work answer
404 not_found. That is a community listing's card, its full detail and its clone. It is also a template that someone else listed on the marketplace, by its page (GET /v1/templates/:slug) and its clone (POST /v1/templates/:slug/clone), and the gallery's report route. A template in the tutorial channel (GET /v1/tutorials) can still be read and cloned by anyone. - Links shared before you set the key stop working for everyone but the owner. A workflow read by link (
GET /v1/public/workflows/:id, which takes no sign-in) answers as a workflow that is not shared. Its editable copy, from a shared production, is refused to anyone it is not otherwise shared with. A public Cine shot (GET /v1/shots/:id) reads as a private one. The share setting itself stays as its owner set it. Only the reads change. - A published app or component is its creator's alone. Its page (
GET /v1/app/:slug), its link card and its embed answer everyone else as for an app that does not exist. So does a run of it started by anyone else:POST /v1/app/:slug/run, Restore, Retry and Render final, and the MCP toolsget_app_inputs,run_appandretry_app_run. A person's own past runs of someone else's app stay readable, also with MCPget_app_run. - A component runs for its creator only.
POST /v1/component/execute, MCPrun_componentandget_component_inputs, and the editor's Run this node on someone else's component answerComponent not found. A component placed in a workflow still runs in that workflow's full runs, whoever owns it. - Another person's job by its id reads as not found. That covers the MCP tools
get_assetanddisplay_asset, the items thatlist_favoritesreturns, and adding a favorite (POST /v1/gallery/favorite, MCPfavorite_asset). You can favorite only your own jobs.
Work marked public before you set the key, or carried over in a copied database, is not served either. A person's own gallery, jobs, apps, components, templates, shots and shared workflows are unchanged for that person: GET /v1/apps/mine and GET /v1/templates/mine still list their own.
The key does not close these:
- The presentation share links (
/v1/present/:token), a separate feature. - A component placed in a workflow. Someone who already knows a component's name can still run it by placing it in a workflow of their own. With every listing closed, nobody can find a new one.
- The catalogs that an administrator curates: the tutorials, the Keepers shelf, the trends and the shared library.
- A file's address in your storage. Whoever holds the address can still read the file.
Provider words
When a job fails, the API can carry the provider's own words about the failure. They appear as error_detail on a job, as errorDetail on a failed node of a run and in the MCP job envelope, and as providerReason on the MCP diagnose_run tool. See The provider's own words.
jobs.errorDetail sets who reads them:
"owner"(the default, and what an absent key means): the job's owner and the administrators."admin": administrators only. Every other reader sees a failed job exactly as one the provider said nothing about:error_detailisnull, a node's entry has noerrorDetail, and there is noproviderReason. The keys stay the same, so no client needs a change.
Any other value, and a jobs value that is not an object, reads as "admin", with a warning. The key stays on the server and is not sent to the browser.
The server enforces the key on every surface that carries the words:
- The job reads:
GET /v1/jobs,GET /v1/jobs/:id,GET /v1/jobs/:id/status,GET /v1/jobs/statusandPOST /v1/jobs/batch-status. - A run's node states:
GET /v1/workflow-executions/:id, its stream, the run lists and a shared workflow's run status. - The MCP tools
get_job,wait_for_job,list_jobs,get_assetanddiagnose_run. - The
job.failedwebhook event. A body recorded before you set the key loses the words too, on a retry and in the endpoint's delivery log.
An administrator is an account with the role admin or super_admin. The job and run reads go by the role of an account signed in to the app. So a read through MCP, a personal API token or an app's token is never an administrator's there. The admin-only reads keep the words, also with an administrator's own token. A webhook delivery is never an administrator's.
Whatever the key says, the words go only to a reader who may see them. A job's words go to its owner, never to another user. A run's words go only to a reader who can open the workflow that the run executed, never to a published app's runner or a person who runs someone else's share link.
The key governs what the server serves from a job and from a run. Words that an editor already saved onto a workflow's nodes stay in that workflow, and anyone who can open the workflow reads them with it. A published app's copy of the workflow leaves them out.
Link imports
With "imports":{"links":false}, users may not import media from a link to another site. The editor hides its link fields, and the server enforces the rule whatever a client sends. An import of a link to another site answers 403 link_imports_disabled, before any job or download starts.
- Accepted values.
true,false,"true","false","1"and"0". The default istrue. Any other value, or animportsvalue that is not an object, reads asfalse. - Your own storage. A file in the install's own storage is not a link to another site and is never refused.
- What it covers. Every import from a link. That is video downloads, YouTube audio, picture and recording imports, Save to Storage, media processing, Trim Audio and Transcribe. It is also Suno Cover and Suno Upload Extend with a link as the source, a dub's
sourceUrland the media probePOST /v1/video-metadata. In a workflow, it covers the Video URL node and the media that a workflow import copies in. A workflow import still succeeds and reports the media as skipped. The same routes through MCP, the SDK and the CLI are refused too. - What it does not cover yet. An outside URL given to a generation or editing route as its source image, video or audio, for example the
imageUrlofPOST /v1/generate-video. Also the image display proxy, Web Scrape and Site Capture, the length probes, publishing or scheduling a post from an outside file, and the relay to Nodaro Cloud. Those URLs still reach the provider or your own network, so keep a network filter for them.
Your own wording
strings replaces interface strings of the editor with your own wording, per language, without a rebuild:
"strings": {
"flow": {
"en": { "auth.tagline": "Visual workflows by Example Studio" },
"he": { "auth.tagline": "תהליכים חזותיים של Example Studio" }
}
}- Product. The first level names the product.
flowis the workflow editor. - Keys. The keys are the editor's own message keys. The editor's build lists them, without their text, in
ui-strings-manifest.flow.json, next to itsindex.html. Every key whose English text is not empty can be replaced. - Languages. An override replaces one string in one language. An
enoverride is also the English that a language without its own translation of the key falls back to. - Checks. The container checks every entry at boot, when it writes
/config.js. An entry that fails a check is dropped with a[surface-profile] strings…warning in the log. The other entries are kept, and a bad entry never stops the install from starting.
An entry is dropped when:
- Its key is not one of the editor's keys.
- Its language is not offered: one of
locale.offered, or without that key, one of the editor's languagesen,he,ja,koandpt-BR. - Its text is empty, longer than 1,000 characters, or not plain text. Plain text has no HTML tag, comment or character reference such as
&, because the editor shows text exactly as written. It has no control character other than a line break, and no bidi embedding or override character (U+202A to U+202E). The marks and isolates that right-to-left text uses are fine. - Its
{placeholders}are not exactly those of the English string. Every placeholder stays and none is added, but their order may change.
Size limits: one product's overrides may hold at most 64 KiB of text, or the whole block is dropped. If the profile with all its overrides would pass 120 KiB, every override is dropped, and nothing else.
Your wording shows from the editor's first render. Messages that the server sends, and the names of nodes, inputs and models, are not message keys and are not covered. An upgrade can remove or rename a key. Your override for it is then dropped with a warning, so read the boot log after each upgrade.
The brand in the page head
At start, right after it writes /config.js, the container writes your brand into the built index.html. Search engines, link previews and the browser tab then show it before any script runs:
<title>takesbrand.productName, unless that isNodaro.<meta name="description">takesbrand.description.- The icon links take
brand.favicon: one path on your site, or a list of up to four, such as["/brand/favicon.svg", "/brand/favicon.png"]. Each path becomes a<link rel="icon">, typed from its extension, and every platform icon link is removed, the home-screen icon included.
Notes:
- Paths only. A path to another site (
https://…or//…), or a path with spaces, quotes or angle brackets, is dropped with a warning. - Ship the files yourself, in your static-file layer. Give them versioned names, because static images are cached for a year.
productNameis required. Abrandblock withoutproductNameis ignored as a whole.- The rest of the file stays as built. If the file has lost one of the three, for example it has no
<title>or two descriptions, the container leaves it as built and logs[start.sh] brand head NOT applied: …. With no brand set, the file is not written at all. - A request for
/favicon.icoitself, which some crawlers send, still gets the platform's icon. - The head is written on every container start. Each new container starts again from the built file.
A Hebrew-first install with its own name, description, icons and sign-in tagline:
NODARO_SURFACE_PROFILE={"brand":{"productName":"Example Studio","description":"Workflows by Example Studio","favicon":["/brand/favicon.svg","/brand/favicon.png"]},"locale":{"default":"he","offered":["he","en"]},"strings":{"flow":{"en":{"auth.tagline":"Visual workflows by Example Studio"},"he":{"auth.tagline":"תהליכים חזותיים של Example Studio"}}}}Frequently asked questions
How do I switch a self-hosted Nodaro from Community to Business?
Set EDITION to business and the VITE_EDITION build argument to business for the nodaro service in docker-compose.community.yml, then build the image from source and start it. The database needs no change.
Why must I rebuild the image to change the edition?
The editor reads its edition when the image is built, and the published image is built as the Community Edition. The API reads EDITION at startup, but the browser app only shows the admin panel in a Business build.
What is a surface profile?
A JSON setting, NODARO_SURFACE_PROFILE, that narrows what a Business or Cloud install shows, without a rebuild. It can hide sidebar entries and home-screen sections, remove nodes and models, rename the product, restrict sign-in methods and force outputs private.
Does the Community Edition read NODARO_SURFACE_PROFILE?
No. The Community Edition ignores the variable and always shows the full interface. Set EDITION=business to use a surface profile.
Can a surface profile enable a feature that the edition does not have?
No. A profile can only narrow. It never enables something that the edition disables.
Can I replace the wording of the editor on a self-hosted install?
Yes, on the Business edition. The surface profile's strings key replaces interface strings per language, without a rebuild. The container checks every entry at boot and drops an invalid one with a warning in the log.
Related
First user and admin
Configuration
Single sign-on
License
Editions
Last updated on
First user and admin
Sign up the first user of a self-hosted Nodaro and promote an admin on the Business edition with one SQL statement, with or without a Supabase dashboard.
Configuration
Every environment variable of a self-hosted Nodaro, grouped by purpose, with its default, whether .env can set it on the compose stack, and what it changes.