Project configuration manifest

volcano-config.yaml is the declarative manifest for a project's user-facing configuration.

volcano-config.yaml is the declarative manifest for a project's user-facing configuration. The CLI uploads it with volcano cloud config deploy, downloads the current state with volcano cloud config pull, and the dashboard offers the same download on the Projects page. Without cloud, both commands target local development. All reconciliation happens server-side through two endpoints:

  • GET /projects/{id}/config — export current configuration (JSON, or canonical YAML with Accept: application/yaml / ?format=yaml)
  • PUT /projects/{id}/config — validate and apply a manifest (?dry_run=true reports projected actions without changing anything)

The manifest applies to the currently selected project (volcano use <name> / VOLCANO_PROJECT_ID). Creating or selecting a project is not part of the manifest.

GitHub auto-deploy reads the same file from your repository, but applies only the functions[] settings that belong to the code a push deploys: visibility, invocation_mode, http_auth_mode, openapi_spec, variable_scope, and variables. Every other section still needs volcano cloud config deploy.

Full schema (version 1)

version: 1

project:
  name: my-app # optional rename
  all_regions: false
  selected_regions: [us-east-1, us-west-2] # bare region names; subset requires SUPERAGENT

databases: # must already exist; assertion-only
  - name: appdb # required, matches deployed database
    region: us-east-1 # required; mismatch => error
    pg_version: '18' # required; mismatch => error
    database_type: volcano-db-xs # asserted; tier changes are NOT allowed via config

frontend_shared_variables: # complete list for shared frontends
  - NEXT_PUBLIC_API_URL

variables: # ALWAYS fully synced when declared
  - name: STRIPE_SECRET_KEY
    value: ${STRIPE_SECRET_KEY} # ${ENV} interpolated by the CLI
  - name: NEXT_PUBLIC_API_URL
    value: https://api.myapp.com

buckets: # must already exist (never created here)
  - name: avatars
    file_size_limit: 5242880 # bytes, optional
    allowed_mime_types: [image/png, image/jpeg]
    policies: # fully synced when declared; omit the key => policies untouched
      - name: public-read
        operation: SELECT # SELECT|INSERT|UPDATE|DELETE
        definition: 'true'

realtime:
  enabled: true
  broadcast_enabled: true
  presence_enabled: true
  postgres_changes_enabled: true

notifications: # omitted fields keep their value
  email:
    enabled: true
    recipients: [] # the project owner; no other address yet
  webhook:
    url: https://ops.myapp.com/hooks/volcano # "" removes it; the secret is never in the manifest
  scheduler_failures:
    notify_on: consecutive_failures # first_failure | consecutive_failures
    threshold: 3 # 1-100
    notify_on_recovery: true

auth:
  tokens:
    access_token_lifetime: 3600
    refresh_token_lifetime: 2592000
    refresh_token_reuse_interval: 10
    platform_token_ttl: 2592000
  sessions:
    inactivity_timeout: 0
    max_session_duration: 0
  signup:
    enable_signup: true
    enable_anonymous_signins: false
    allowed_email_domains: [] # empty = any domain may sign up
    allowed_email_domains_mode: signup # signup | signup_and_signin | disabled
  rate_limits: # per hour
    signup: 100
    signin: 100
    token_refresh: 1000
    password_reset: 10
  password:
    min_length: 15 # 15-128
    require_uppercase: true
    require_lowercase: true
    require_numbers: true
    require_special_chars: false
  password_reset:
    allow: true
    timeout: 3600
    max_history: 0
  email_verification:
    require_confirmation: true
    confirmation_timeout: 86400
  cors:
    enabled: true
    allowed_origins: [https://myapp.com, http://localhost:3000]
    allow_credentials: true
    max_age: 86400
    allow_frontend_previews: true # allow this project's live preview origins
  providers:
    email_password:
      enabled: true
    oauth: # fully synced when declared
      - provider: google # google|github|microsoft|apple|device
        enabled: true
        client_id: ${GOOGLE_CLIENT_ID}
        client_secret: ${GOOGLE_CLIENT_SECRET} # write-only
        redirect_url: https://api.myapp.com/auth/oauth/google/callback
        scopes: [openid, email, profile]
      - provider: device # client_id/secret server-generated
        enabled: true
  email:
    enabled: true
    from:
      address: no-reply@myapp.com
      name: My App
    smtp:
      host: smtp.sendgrid.net
      port: 587
      username: ${SMTP_USERNAME}
      password: ${SMTP_PASSWORD} # write-only
      use_tls: true
    templates: # fully synced when declared
      confirmation:
        subject: 'Confirm your email'
        html_body: '<p>Confirm: {{.Token}}</p>' # bodies require SUPERAGENT
        text_body: 'Confirm: {{.Token}}'
      password_reset:
        subject: 'Reset your password' # subject-only works on HOBBY
      password_changed:
        subject: 'Your password was changed'
      welcome:
        subject: 'Welcome'
        html_body: '<p>Welcome {{.Email}}</p>'
        text_body: 'Welcome'
  managed_pages:
    enabled: true
    redirects:
      allowed: [https://myapp.com/welcome, https://myapp.com/login]
      post_auth: https://myapp.com/welcome
      post_logout: https://myapp.com/goodbye
      device_verification: https://myapp.com/device
    pages: # SUPERAGENT; upsert-only (omission => untouched)
      login:
        html: '<html>...</html>' # required, <=256 KiB
        css: 'body{}' # optional, <=256 KiB
      reset_password: # maps to page type "reset-password"
        html: '<html>...</html>'
      signup: # also: forgot_password, device, verify_email
        html: '<html>...</html>'
    appearance: # SUPERAGENT; built-in renderer configuration
      theme: # shared by every managed auth page
        version: 1
        colors:
          background: '#f7f5f2'
          surface: '#ffffff'
          text: '#1a1a1a'
          accent: '#1d4ed8'
          accent_text: '#ffffff'
        font: humanist # system|humanist|geometric|slab|mono
        scale: large # small|default|large
        density: spacious # compact|comfortable|spacious
        radius: large # none|small|medium|large
      layouts: # centered|split-left|split-right
        login: split-left
        signup: split-right
        forgot_password: centered
        device: split-left
        verify_email: centered
        reset_password: split-right

functions: # must already be deployed
  - name: hello
    kind: standard # standard (default) or durable; fixed at creation
    visibility: public # private, authenticated, or public; omit to keep the current level
    invocation_mode: http # rpc (default) or http
    http_auth_mode: none # volcano (default) or none; none requires public
    openapi_spec: # optional OpenAPI 3.0/3.1 metadata; HTTP mode only
      openapi: 3.1.0
      info: { title: Hello webhook, version: 1.0.0 }
      paths: {}
    variable_scope: scoped # all (default) or scoped
    variables: # replaces the declared list when present
      - STRIPE_SECRET_KEY
    schedulers: # fully synced when declared
      - name: nightly
        cron: '0 3 * * *' # 5-field UTC cron
        enabled: true
        payload: { source: cron }

frontends: # must already be deployed
  - name: web
    variable_scope: shared # all (default), shared, or scoped
    function_routes: # fully synced when declared
      - function: hello # public standard HTTP-mode function
        path_prefix: /api # exact segment prefix; /api or /api/...
        strip_prefix: true # function receives / for /api
    frontend_routes: # fully synced when declared
      - frontend: marketing # another frontend of the project
        path_prefix: /blog # forwarded unchanged; /blog or /blog/...
    custom_domain: # omitted: kept; null or bare key: deleted
      domain: app.myapp.com
      tls: # optional for an existing domain
        mode: managed
  - name: marketing
    custom_domains: # instead of custom_domain; fully synced when declared
      - domain: www.myapp.com
        tls: { mode: managed }
      - domain: myapp.com # a second domain requires SUPERAGENT
        tls: { mode: managed }
        redirect: # answer with a redirect instead of serving
          to: www.myapp.com # a serving domain listed here
          status: 308 # 301, 302, 307, or 308 (default)

Not in the manifest: project logo, anon/service keys, project access tokens, auth users (no end-user creation, bans, or sessions), scheduler regions, code artifacts (function/frontend deploys), and one-shot actions (password reset, key regenerate, test email, redeploy).

Credentials stay out because they are state, not configuration: they are created and revoked imperatively, and a project access token's secret exists only in the response that created it. Manage them through their endpoints.

Reconciliation semantics

  • Omitted sections are untouched. Only what you declare is reconciled.
  • Patch semantics within entries. An omitted optional field keeps its current server value (for example a bucket entry with only file_size_limit leaves allowed_mime_types alone).
  • Function visibility. visibility is private, authenticated, or public; see who can invoke a function. Omitting it keeps the current level. public is a deprecated alias that still applies: public: true means public and public: false means authenticated, not private. A manifest that sets both must make them agree. Export writes visibility and never public. Exports made before visibility existed wrote public: false, and an entry that still says so applies authenticated on every deploy and every push. To keep the function private, replace it with visibility: private; volcano cloud config pull writes visibility for every function.
  • Function invocation metadata. invocation_mode, http_auth_mode, and openapi_spec use the same validation as PATCH /projects/{id}/functions/{functionId}. Unauthenticated HTTP ingress (http_auth_mode: none) is allowed only when the function is public. Moving such a function off public implicitly restores http_auth_mode: volcano when the auth mode is omitted.
  • Frontend Function routes. Declaring frontends[].function_routes fully syncs the same-origin path mappings for that frontend; routes absent from the list are deleted. Omitting the key preserves existing routes, while function_routes: [] deletes them all. Targets must be deployed standard Functions with visibility: public and invocation_mode: http; one apply can set both on the function and add the route, and a route to a function that stays non-public is a validation error. A function that keeps a route cannot be made non-public or moved to rpc unless the same apply removes that route through the frontend's function_routes. A route works on both the generated frontend hostname (including preview deployments) and a custom domain because routing follows the resolved frontend rather than the domain. An apply may remove a function's routes and change its visibility or mode together; Volcano removes the routes before changing the function. See Frontend Function routes.
  • Frontend routes. Declaring frontends[].frontend_routes fully syncs the paths that frontend serves from another frontend of the project; routes absent from the list are deleted. Omitting the key preserves existing routes, while frontend_routes: [] deletes them all. Each frontend must already exist and can't be the frontend itself. Function and Frontend routes on one frontend share one set of paths, so a path listed in both is a validation error, as is a path the other kind already routes when the manifest leaves that kind alone. An apply removes routes before it adds any, so it can move a path from one kind to the other. An export includes frontend_routes only on frontends that have routes. Where GET /capabilities lists frontend_routes as unavailable, a manifest that would create or change a route answers 400 with code feature_unavailable; keeping or removing routes still applies. See Frontend routes.
  • Function kind is asserted, never written. kind is fixed when a function is created, so the manifest compares it and reports an error when it disagrees with the deployed function. Omitting it means standard, so name kind: durable to describe a durable function. An export writes kind: durable for durable functions and leaves it off standard ones, so re-applying an export is a no-op either way.
  • Durable functions take no invocation settings. invocation_mode, http_auth_mode, and openapi_spec describe synchronous HTTP invocation, which a durable function does not have — it is started through its executions collection. Declaring one on a durable function is a validation error, and an export never writes them. visibility still applies: it decides who may start executions. See Durable functions.
  • Shared variables. shared_variables: [LOG_LEVEL] replaces the complete shared list using existing names only. Omission keeps membership; [] clears it. Apply changes no values and deletes no variables through this field. Re-applying the same list retries function environment reconciliation, including after a previous apply failed to start it. Export emits the shared names and omits variable values. Unknown names and oversized final function environments are rejected before mutations. Scoped functions select variables independently of this list. The variable API's optional shared write field preserves existing membership when omitted and defaults to true for new variables for legacy clients. In a manifest that declares shared_variables, that list is the complete membership and is authoritative for every variable in the apply. Each create and update is written with the membership the list gives it, never as shared first and demoted after, so no reader sees a name under a membership the manifest did not ask for. A conflicting per-variable shared does not override the list — declaring shared_variables: [] alongside shared: true on an entry writes that variable private. Use the per-variable field only when the manifest omits shared_variables.
  • Function variable scope. variable_scope: all (the default) gives a function only variables marked shared: true. variable_scope: scoped gives it only the variables it selects: every name in variables, plus the names Volcano detects in its source that the project defines. Omitting either key keeps the function's current value, so a function scoped through an earlier apply stays scoped. See Function variables below.
  • Frontend shared variables. frontend_shared_variables replaces the complete project list for frontends with variable_scope: shared. Omission keeps the list; [] clears it. Unknown names and environments over 4096 bytes are rejected before mutation. all keeps the legacy access to every project variable. scoped uses only the frontend entry's variables list.
  • Fully synced when declared (destructive by design): variables, buckets[].policies, auth.providers.oauth, auth.email.templates, functions[].variables, functions[].schedulers, and frontends[].custom_domains. The declared list is the source of truth: entries absent from the manifest are deleted; an explicit empty list deletes everything. Run volcano cloud config pull before your first deploy and check the --dry-run report to see what a partial list would remove.
  • Email templates and built-in defaults. Template content that still matches the built-in defaults is not a customization: exports omit the default bodies, and a declared templates map treats them as absent, so a subject-only entry works on HOBBY without touching the default content. A template type absent from a declared map reverts to the built-in default.
  • Never created, never deleted: functions, frontends, databases, and buckets. The manifest only updates their configuration. Databases are assertion-only — region, pg_version, and database_type are compared, never written; a database_type mismatch is an explicit error because tier changes must go through volcano cloud databases or the dashboard.
  • Custom domains belong to their declared frontend entry. Set tls.mode: managed to have Volcano issue and renew the certificate. Your account must own the hostname's domain in either TLS mode; see Verify domain ownership. Both an apply and a dry run refuse a hostname another account owns, and a BYOC hostname your account has not verified, naming the _volcano TXT record to publish. When DNS does not answer an ownership check, they refuse the hostname with a message to retry later. A managed hostname nobody owns is reserved for your account until you publish the record that volcano cloud frontends domain get reports; then keep the certificate validation CNAME in DNS for renewal. Domains already attached keep applying unchanged. If another account has only reserved the hostname, an apply that adds a domain to a frontend without one releases the reservation once your account owns the domain; for a managed domain it otherwise reports your account's _volcano TXT record, so publish it and apply again. A reservation is not ownership, so a dry run reports that domain as created and only the apply names the record. Replacing an existing domain never takes over a reservation: delete the existing domain in one apply, wait for its removal to finish, then declare the hostname. Add the separate routing record to direct traffic to the frontend. Use CNAME unless the hostname is the apex of your DNS zone, including a separately delegated subdomain. At an apex, use your provider's ALIAS, ANAME, or CNAME-flattening feature. custom_domain describes a frontend with at most one domain and no redirect; for any other frontend an apply that sets it fails validation and names custom_domains. Omitting custom_domain on a declared frontend leaves its domain alone, so a domain attached with volcano cloud frontends domain create survives a manifest that does not mention it. The report lists it as unchanged with a notice naming the hostname. To delete the domain, set custom_domain: null; the report names the hostname it deletes. Manifests that relied on omission to delete a domain need the explicit null. Moving a hostname to another of the project's frontends takes two applies: set custom_domain: null on its current frontend, wait for that removal to finish, then declare it on the new one. After a served domain is removed, proxies can keep serving its hostname for about a minute. An apply that declares that hostname within the minute fails validation; apply again once it has passed. Changing the hostname of a managed domain requires one apply to delete it and a later apply to create the replacement. Replacing a BYOC domain with a managed domain for a different hostname takes one apply; if Volcano cannot admit the new hostname, the BYOC domain keeps serving. Changing the TLS mode of an active domain for the same hostname takes one apply and keeps HTTPS up: the current certificate serves until its replacement does. See Switch between BYOC and managed TLS. A domain that is not yet active cannot switch in place; delete it first. Managed domains retain their certificate quota until deletion finishes. At the account limit, apply the deletion, wait for cleanup, then apply the replacement; planning a deletion does not grant capacity early.
  • Custom domain sets. frontends[].custom_domains lists every custom domain of the frontend, each with domain, tls, and an optional redirect (to, status). Declaring it fully syncs the set: listed domains are created or updated, unlisted ones are deleted and the report names each hostname, and custom_domains: [] deletes them all. Omitting the key leaves the domains alone. An entry sets custom_domain or custom_domains, not both. Each entry follows the custom_domain rules above for ownership, TLS, and rotation; where they say to set custom_domain: null, drop the entry from the list instead. A redirect.to must name another listed domain that is active and has no redirect, and status defaults to 308. A domain the apply adds is not active yet, so add redirects to it in a later apply. One apply can add, remove, and retarget redirects among active domains, including swapping which domain serves: Volcano orders the changes so no redirect points at a domain that isn't serving. Replacing a hostname in one apply stops the old one serving as soon as its removal starts, before the new one is active. To swap without downtime, apply the list with both, wait for the new domain to be active, then apply the list without the old one. When the plan's allowance holds the old and new domains together, the apply creates the new domain first and keeps the old one if the create fails; the report marks the old entry as an error, and the next apply retries both. If the create stopped before Volcano stored the certificate, the retry needs certificate_pem and private_key_pem in the entry. A list longer than the plan's custom domains per frontend fails validation when it adds a hostname, so a frontend over the limit after a downgrade still re-applies its export and can shrink. Export writes custom_domains for a frontend with more than one domain or any redirect, and custom_domain otherwise. See Redirect one domain to another.
  • BYOC certificates use tls.mode: byoc with certificate_pem and private_key_pem together, plus optional certificate_chain_pem. A tls block without mode is BYOC. New material for the same domain rotates the certificate in place with zero downtime: the frontend proxies pick up the new certificate within seconds while the old one keeps serving handshakes. Omitting tls, or sending only tls.mode: byoc as an export does, keeps the stored certificate. A different BYOC domain name is a detach-and-recreate; the new domain serves after provisioning.
  • Hosted pages are upsert-only. There is no delete API for hosted pages, so pages omitted from managed_pages.pages are left untouched.
  • Managed-page appearance is also upsert-only in the manifest. Omitted theme or layout fields stay untouched. Use the appearance API's DELETE operations to return a theme or page layout to default tracking.
  • auth.signup.allowed_email_domains replaces the stored list when declared. [] removes the restriction; omitting the key keeps the current allowlist. Entries are normalized to bare lowercase domains, so ["@Acme.com"] is stored as ["acme.com"]. allowed_email_domains_mode follows the same rule — omit it to keep the stored mode. Declaring signup_and_signin, or narrowing the list while it is already set, signs out every session the list no longer admits. See Email domain allowlist.
  • New or changed auth.email.from.address and name values cannot contain line breaks. Apply reports invalid values as 422 validation errors. Exports remove leading and trailing whitespace from older sender settings; reapplying them is a no-op and leaves the stored settings unchanged. Unchanged exports of legacy values with interior line breaks also preserve those values; delivery refuses an unsafe stored sender until it is repaired.
  • New or changed email template subjects cannot contain literal interior line breaks after Go template whitespace trimming. Apply reports one 422 validation error under the affected template entry. Multiline control flow with {{- and -}} trim markers, outer whitespace, and trailing newlines from YAML block scalars are allowed; message bodies can span lines. Exports preserve older subjects. Reapplying the exact stored value is a no-op, and unrelated settings or template bodies remain editable. A changed subject must pass validation. Delivery uses the default subject when a rendered legacy value is unsafe. See Subject headers.

Leave out lavaflow

The manifest doesn't configure LavaFlow. A manifest that still has a lavaflow section applies as if the section were absent: the dry run and the apply list it as unchanged with a notice to remove it, and export never writes it. That includes autonomy: merge and merge_policy: letting LavaFlow merge is the project owner's decision, made with PUT /lavaflow/settings.

Notification settings

The notifications section reconciles where the project's alerts go. An omitted field keeps its current value. Alert emails go only to the project owner's verified account address, so leave recipients empty. Naming that address also works, but the file then fails validation once the address changes. Any other address is a validation error, and a dry run reports it. The webhook signing secret is never part of the manifest. Declaring a webhook.url on a project without a webhook generates a secret the apply does not return. To get a secret you can use, rotate it with POST /projects/{id}/notifications/webhook-secret. url: "" removes the webhook and its secret.

Export writes the section once the settings have been saved. It never writes webhook, because the URL can carry your receiver's token. An omitted webhook keeps its URL and secret, so re-applying an export leaves it in place; declare url: "" to remove one.

Coverage warnings: skipped and missing

Because the manifest can never create functions, frontends, databases, or buckets, the apply report tells you when your file and your deployed resources disagree:

  • skipped — the manifest configures a resource that does not exist. Deploy or create it first, then re-run volcano cloud config deploy.
  • missing — a deployed resource has no entry in its declared manifest section.

Both are warnings: the rest of the manifest still applies and the CLI exits 0.

For a new frontend, deploy it with the variables its entry selects, then apply the manifest (see First deploy with volcano-config.yaml):

volcano cloud frontends deploy --variable-scope scoped --variable NEXT_PUBLIC_API_URL
volcano cloud config deploy

volcano cloud deploy does both for every frontend in .volcano/source-layout.json, after the functions, and applies the manifest last. Pass -c to apply another manifest, and repeat it to apply several in order. It refuses a manifest with a variables section when an env file is also present, since applying the section would delete the variables only the env file sets. See the CLI deploy reference.

Validation, plan limits, and partial failures

  • Validation failures (bad cron expressions, invalid policy definitions, database assertion mismatches, plan-gate violations) return 422 with the full error list and nothing is applied.
  • Plan gates are change-aware: they only fire when the manifest would change a gated value. Re-applying an export of a project downgraded from SUPERAGENT stays a no-op.
  • SUPERAGENT-gated surfaces: region subsets, email template bodies, hosted pages, and the auth.signup.allowed_email_domains allowlist. A HOBBY project can still declare the allowlist it already has, or clear it to remove the restriction. Scheduler counts, storage policy counts, custom domains per frontend, and managed TLS certificates respect plan caps.
  • Apply-phase failures (a provider call failing mid-apply) return 200 with per-entry action: error; already-applied changes are not rolled back. Re-running the deploy is safe — unchanged entries are no-ops.
  • Applies are serialized per project; a concurrent apply returns 409.

Function variables

By default a function receives the project variables marked shared: true. Keep this list small: the platform caps a function's environment at 4096 bytes, measured as its variables encoded as JSON, and a project can hold more than that. See environment variables for how the size is counted.

Scoping a function narrows it to the variables it actually reads:

functions:
  - name: checkout
    variable_scope: scoped
    variables:
      - STRIPE_WEBHOOK_SECRET

The function's variables come from two sets, which differ in whether the name has to exist:

  • Declared — the names in variables. Required: a declared name the project does not define fails the apply. These round-trip through volcano cloud config pull, so the manifest stays the source of truth for them.

  • Detected — the names Volcano finds in the source you deploy. Optional: a detected name is included when the project defines it and ignored when it does not, because a direct reference is often to something optional (process.env.DEBUG, process.env.NODE_ENV) and code written to run without it should still deploy. Volcano reads direct references only:

    RuntimeDetected forms
    Node.jsprocess.env.NAME, process.env["NAME"]
    Pythonos.environ["NAME"], os.getenv("NAME")
    RubyENV["NAME"], ENV.fetch("NAME")

    Detection runs on every deploy and reads the whole source you uploaded, so it follows your code without a manifest change. It reads code only: a name that appears just in a comment or inside an unrelated string is not a reference and does not select a variable. Code interpolated into a string still counts, so ${process.env.KEY}, #{ENV['KEY']} and f"{os.getenv('KEY')}" are detected. It skips dependency trees (node_modules, vendor, .venv), so declare a variable your function only reads from inside a dependency.

Detected names are remembered even when the project does not define them yet, so creating the variable later redeploys the function with it. Declare a name in variables when the function must not deploy without it — that is the difference between the two sets.

Anything computed — process.env[key], a name built at runtime — cannot be detected. Declare those in variables. A scoped function reading a variable that is neither declared nor detected gets undefined at runtime, even when the project defines it; that is the cost of not failing a deploy over an optional reference.

Apply checks every function against the variables the manifest will produce, not the ones the project has now, so one file can create a variable and declare it on the same pass. It rejects the apply before any change lands when:

  • A scoped function declares a variable the resulting project does not define — including one this file deletes, even if that function's own entry is unchanged. Create the variable, or drop the name.
  • A function's resulting environment exceeds 4096 bytes. Scope the function to fewer variables, or shorten the values.

volcano cloud config deploy and a Git push refuse a variables key with no value, such as a bare variables: or variables: null, rather than reading it as omitted. Write variables: [] to declare no names, or remove the key to keep the current list. volcano cloud config deploy answers 400, and a Git push fails its deploy with nothing applied.

Changing variable_scope or variables redeploys that function's environment, so switching from all to scoped removes the variables it no longer selects from the running function rather than only from its configuration.

Creating, updating, or deleting a project variable only redeploys the functions that select it. A function scoped away from a variable is left alone, so a write to one secret no longer restarts every function in the project.

Secrets

Write-only secrets — auth.email.smtp.password, OAuth client_secret, and BYOC certificate material — are omitted from exports and stay unchanged unless you set them explicitly. The notification webhook signing secret is never part of the manifest either; see Notification settings. Custom-domain exports keep only tls.mode, for both managed and BYOC domains. The CLI interpolates ${ENV_VAR} references before upload, so keep secrets in your environment, not in the file. Variable values are included in exports (they are readable through the API).

Asynchronous side effects

Apply starts long-running work and returns immediately: variable changes propagate to functions and frontends through one batched workflow, region changes trigger function redeploys, and new custom domains provision asynchronously. Check convergence with the usual commands (volcano cloud variables list, volcano cloud frontends domain get, ...).

Frontend variable selection

Limit the project variables available to an existing frontend during both build and runtime:

version: 1
frontends:
  - name: web
    variable_scope: scoped
    variables:
      - NEXT_PUBLIC_API_URL
      - SESSION_SECRET

New frontends default to scoped with no selected project variables. Existing frontends created under the legacy all default keep that selection until it is changed explicitly; this legacy mode selects every project variable and does not use the shared function list. scoped selects only the declared names; frontend source is not scanned for variable references. Missing declared variables reject deployment or apply. NEXT_PUBLIC_* names remain build-only and do not enter the runtime environment.

Omitting either field preserves its stored value; variables: [] selects no project variables in scoped mode. Config export includes both fields.

A scope change synchronizes the running cloud frontend's environment. Rebuild and redeploy to apply changes to values embedded in browser assets. Local mode uses the stored selection on deployment and restart. Variable updates that the frontend does not select do not trigger its runtime synchronization.

Selected runtime values, platform metadata, and reserved space for the largest cache metadata and proxy-token rotation must fit the 4,096-byte environment limit before a variable or scope change is saved. The platform's share is about 900 bytes, reserved even when no rotation is staged, so roughly 3,100 bytes remain for selected variables. The reservation keeps updates safe without reading deployed runtimes. Deployment also checks the final environment including platform-managed values; an oversized environment fails before the runtime configuration is updated. Error messages report byte counts without exposing variable values.

Sandbox template settings

Declare existing sandbox templates by name:

version: 1
sandboxes:
  - name: browser-worker
    memory_mb: 1024
    ports: [8080]
    ttl_seconds: 3600

config deploy --dry-run previews changes. Applying updates the default lifetime for newly created sessions; existing sessions keep their original lifetime. An explicit session max_duration_seconds overrides the template default. Cloud sessions allow 30–3,600 seconds on HOBBY and 30–28,800 seconds on SUPERAGENT, subject to regional capacity. Template defaults above the plan limit are clamped when starting a session; an explicit duration above it is rejected. Local lifetime defaults to zero (unlimited); positive values impose a limit without the cloud eight-hour limit. Local commands also default to unlimited.

memory_mb (1024 or 2048) and ports describe the deployed image. Running a 2048 MiB image requires SUPERAGENT; HOBBY supports a 1024 MiB baseline. Image deployment and validation can succeed on HOBBY at either size, but starting a 2048 MiB session or one-shot execution returns sandbox_plan_limit_exceeded. Config apply asserts these values and reports a validation error if they differ; deploy the sandbox source to rebuild an image with different values. Omitted fields preserve current settings. Missing templates are reported as skipped; templates absent from the manifest are reported as missing and are never deleted. Export includes the current settings and can be applied unchanged. A project without sandbox templates exports no sandboxes section.

Git deployments discover each volcano/sandboxes/<name>/Dockerfile and its isolated build context. Matching sandboxes entries supply memory, readiness ports, and session defaults. Each push records a durable deployment for every discovered sandbox and waits for image validation before reporting success. An entry without a source directory does not create a template. Undeclared source directories use the existing template's settings, or 1024 MB with no service ports for a new template.

On this page