/ Docs

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 config deploy, downloads the current state with volcano config pull, and the dashboard offers the same download on the Projects page. 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: public, invocation_mode, http_auth_mode, openapi_spec, variable_scope, and variables. Every other section still needs volcano 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: aws-us-east-1                   # required; mismatch => error
    pg_version: "16"                        # 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;
      - name: public-read                   #   omit the key => policies untouched
        operation: SELECT                   # SELECT|INSERT|UPDATE|DELETE
        definition: "true"

realtime:
  enabled: true
  broadcast_enabled: true
  presence_enabled: true
  postgres_changes_enabled: 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: 8
    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
  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
    public: true                            # anon-key invocation; anon-key start for durable
    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                     # standard HTTP-mode function
        path_prefix: /api                   # exact segment prefix; /api or /api/...
        strip_prefix: true                  # function receives / for /api
    custom_domain:                          # SUPERAGENT; BYOC TLS only
      domain: app.myapp.com
      tls:                                  # optional for an existing domain
        mode: byoc
        certificate_pem: ${TLS_CERT_PEM}          # write-only
        private_key_pem: ${TLS_KEY_PEM}           # write-only
        certificate_chain_pem: ${TLS_CHAIN_PEM}   # optional, write-only

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 invocation metadata. invocation_mode, http_auth_mode, and openapi_spec use the same validation as volcano functions update. Unauthenticated HTTP ingress (http_auth_mode: none) is allowed only when the function is public. Changing such a function to private 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 configured with invocation_mode: http. 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 it to rpc together; Volcano removes the routes before changing the invocation mode.
  • 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. public still applies: on a durable function it lets an anon key 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, and functions[].schedulers. The declared list is the source of truth: entries absent from the manifest are deleted; an explicit empty list deletes everything. Run volcano 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 databases or the dashboard.
  • Custom domains belong to their declared frontend entry: omitting custom_domain on a declared frontend deletes an existing domain. The same domain with new TLS material rotates the certificate in place with zero downtime — the frontend proxies pick up the new certificate within seconds (cache invalidation, 5-minute TTL backstop) while the old still-valid certificate keeps serving handshakes. A different domain name is a detach-and-recreate: the new domain serves after provisioning/verification.
  • 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 account whose domain the list no longer admits. See Email domain allowlist.

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 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.

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, custom domains, 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 and storage policy counts 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, summed across the names and values it is configured with, and a project can hold more than that.

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 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.

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 custom domain TLS material — are omitted from exports and stay unchanged unless you set them explicitly. 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 variables list, volcano 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. Keep any existing custom_domain declaration when applying the frontend entry, since omitting it removes the domain under the existing reconciliation rules.

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. This conservative reservation includes 1,024 bytes for the next proxy token plus its key, even when no rotation is staged. It 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.

On this page