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 withAccept: application/yaml/?format=yaml)PUT /projects/{id}/config— validate and apply a manifest (?dry_run=truereports 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-onlyNot 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_limitleavesallowed_mime_typesalone). - Function invocation metadata.
invocation_mode,http_auth_mode, andopenapi_specuse the same validation asvolcano functions update. Unauthenticated HTTP ingress (http_auth_mode: none) is allowed only when the function is public. Changing such a function to private implicitly restoreshttp_auth_mode: volcanowhen the auth mode is omitted. - Frontend Function routes. Declaring
frontends[].function_routesfully syncs the same-origin path mappings for that frontend; routes absent from the list are deleted. Omitting the key preserves existing routes, whilefunction_routes: []deletes them all. Targets must be deployed standard Functions configured withinvocation_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 torpctogether; Volcano removes the routes before changing the invocation mode. - Function kind is asserted, never written.
kindis fixed when a function is created, so the manifest compares it and reports an error when it disagrees with the deployed function. Omitting it meansstandard, so namekind: durableto describe a durable function. An export writeskind: durablefor 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, andopenapi_specdescribe 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.publicstill 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 optionalsharedwrite field preserves existing membership when omitted and defaults totruefor new variables for legacy clients. In a manifest that declaresshared_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-variableshareddoes not override the list — declaringshared_variables: []alongsideshared: trueon an entry writes that variable private. Use the per-variable field only when the manifest omitsshared_variables. - Function variable scope.
variable_scope: all(the default) gives a function only variables markedshared: true.variable_scope: scopedgives it only the variables it selects: every name invariables, 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_variablesreplaces the complete project list for frontends withvariable_scope: shared. Omission keeps the list;[]clears it. Unknown names and environments over 4096 bytes are rejected before mutation.allkeeps the legacy access to every project variable.scopeduses only the frontend entry'svariableslist. - Fully synced when declared (destructive by design):
variables,buckets[].policies,auth.providers.oauth,auth.email.templates,functions[].variables, andfunctions[].schedulers. The declared list is the source of truth: entries absent from the manifest are deleted; an explicit empty list deletes everything. Runvolcano config pullbefore your first deploy and check the--dry-runreport 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
templatesmap 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, anddatabase_typeare compared, never written; adatabase_typemismatch is an explicit error because tier changes must go throughvolcano databasesor the dashboard. - Custom domains belong to their declared frontend entry: omitting
custom_domainon 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.pagesare left untouched. - Managed-page appearance is also upsert-only in the manifest. Omitted
theme or layout fields stay untouched. Use the appearance API's
DELETEoperations to return a theme or page layout to default tracking. auth.signup.allowed_email_domainsreplaces 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_modefollows the same rule — omit it to keep the stored mode. Declaringsignup_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-runvolcano 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
422with 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_domainsallowlist. 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
200with per-entryaction: 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_SECRETThe 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 throughvolcano 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:Runtime Detected forms Node.js process.env.NAME,process.env["NAME"]Python os.environ["NAME"],os.getenv("NAME")Ruby ENV["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']}andf"{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_SECRETNew 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.