Project repository and synchronization
Project repository and synchronization
Section titled “Project repository and synchronization”A repository describes remote draft assets, not published snapshots, all account state or a complete backup. push does not publish or run database migrations; pull does not pull the last published release. Read CLI synchronization for exact flags and CLI overview for session discovery. All commands/snippets here are source-derived and unexecuted; fixture names demo, hello refer to the disposable first-endpoint guide, and cli-demo/demo.viatpkg to the CLI examples.
Prerequisites and permissions
Section titled “Prerequisites and permissions”Run from the intended repository root: credential discovery can walk ancestors for .viat/config.yaml, but asset sync uses current working directory, not the discovered config directory. Selected project and viat.yaml.project.slug must match. Verify server/project before writing. Keep a complete backup and review local modifications before pull.
Editor membership (owner, admin, developer) is needed for push/content and for full pull/export’s endpoint/tree/role/authorizer/model reads. Operator membership does not authorize those reads. Metadata stages that change project name/scope claims require Owner/Admin even if content editing is allowed. A filtered sync does not grant additional permissions or make an incomplete checkout safe.
File layout and identity
Section titled “File layout and identity”viat.yamlresources/<folder>/<name>.yamlscripts/<folder>/<name>.<ext>workflows/<folder>/<name>.yamlendpoints/<tree>/<branch>/[id].get.yamlschedules/<name>.yamlwatches/<name>.yamlqueue-triggers/<name>.yamlws_triggers/<name>.yamlim/accounts/<name>.yamlim/triggers/<name>.yamlcrate/Cargo.tomlcrate/src/lib.rsauthz/migrations/<id>.yamlmigrations/<resource>/*.sql<folder> is optional and part of resource/script/workflow identity: scripts/reports/hello.py is reports/hello, not root hello. Asset walks recurse, skip dot-directories and sort paths. Trigger collections are flat rather than folder-namespaced. queue-triggers uses a hyphen on disk; its --only selector is queue_triggers. The SQL migration directory is a separate viat db migrate system; authorization migrations are not SQL.
Script extensions: .py Python, .ts TypeScript, .sql SQL, .sh shell, .graphql GraphQL, .vgql viat_gql, .prompt llm, .rs Rust, .yaml/.yml binary manifest. Unknown extensions are skipped on push. Binary executable bytes are artifacts, not the manifest’s repo source; pulling YAML does not back up a program artifact. Workflows read .yaml only and carry the definition verbatim. Use the actual examples/hello-endpoint/hello.py fixture as script content, not a separately maintained rewritten copy.
viat.yaml: full metadata structure
Section titled “viat.yaml: full metadata structure”Required project:{slug,name}. Optional lists default empty: authorizers, roles, scope_claims; optional/null authz means unmanaged, not an empty model. Old permissions and roles[].permissions are parsed only to refuse nonempty retired catalog data.
project: slug: demo name: Demoauthorizers: - name: machine kind: api_key default_role: public config: token_ttl_hours: 1roles: - name: reader is_admin: false description: Fixture readerscope_claims: - name: tenant type: text source: metaauthorizers[]: requiredname,kind; optional/nulldefault_roleleaves the server role unmanaged; optional/nullconfigleaves config unmanaged. Supplied config is complete replacement, not a merge. Secrets are never pulled. Kind changes are reported/skipped, not delete/recreated; changing role/config in place preserves existing keys. First creation of secret-requiring OIDC/social authorizers needs separately provisioned server secrets rather than hoping an exported config contains them. Platform-createdchatis not repo-owned.roles[]: requiredname; optional/nullis_adminkeeps existing flag, optional/nulldescriptionkeeps description;""clears description. Built-inpublic,system:schedulerare omitted from pull and not pruned by push.scope_claims[]:{name,type,source,from?}; full structure/validation in settings. Omitting the list defaults empty, so mirror push eventually removes server-only declarations; it is not “unmanaged” like omitted authz. Import adds without removal.authz: optionalsubject_attrs,types,rules, each defaults[]. An explicit empty managed object can prune a model. Delegated rules are not serialized or mirrored.
Within authz:
| List | Full item shape |
|---|---|
subject_attrs | name, source, optional from_key, ty, is_set:false default, optional tenant_bypass |
types | name, cross_tenant:false default, optional tenant_attr, tenant_subject, tenant_bypass; attrs:[], actions:[] defaults |
types[].attrs | name, kind; optional column_name, rel_table, rel_local_fk, rel_ref_col, rel_col; is_set:false default |
types[].actions | String action names |
rules | type, action, effect, expr AST; rules keyed by content, not exported server ID |
Example model stanza (append to the fixture metadata only after review):
authz: subject_attrs: - name: role source: role ty: text types: - name: report cross_tenant: true actions: [read] rules: - type: report action: read effect: permit expr: subj_only: {subj: role, op: eq, lit: reader}See authorization for complete AST/tenant semantics. This cross-tenant fixture is not production isolation. No connection secret belongs in viat.yaml.
Resources, secrets and environment-specific values
Section titled “Resources, secrets and environment-specific values”Resource file fields: required name, kind, plain; optional secret; allowed_authorizers defaults []. Parent directories supply folder identity; file basename is not a substitute for body name. Existing resource references own each kind’s complete plain/secret schema; do not transplant credentials from this page.
Pull writes string $secret placeholders in secret fields; legacy YAML !secret markers are still accepted on push. Both are stripped from submitted secret fields, preserving existing secrets. They are not actual credentials. $secret:<name> or {$secret:name} resource references in supported config positions are a different feature.
Top-level plain value $keep preserves that field on an existing server resource; it is not a recursive merge/reset and cannot create a missing value. Pull preserves existing applicable $keep choices. Resource push uses PATCH for existing resources, not wholesale replacement of the entire server object; provided fields follow the resource API’s own patch rules. Resources are never deleted just because files are absent. system/platform-managed resources are skipped, not a platform DB credential export.
On normal push, a new resource with unresolved $secret/!secret or $keep fails. Provision it on the target with secure values first, then use placeholders. Resources of kind secret go before dependent config resources; library resources are saved last so they freeze the just-pushed script version, not its predecessor.
Endpoint files and unsupported round trips
Section titled “Endpoint files and unsupported round trips”Each tree is a directory; branches are directories; :id becomes [id]; leaves are <segment>.<method-lowercase>.yaml. Body method and filename must agree. Endpoint target uses ref, unlike trigger targets’ name:
method: GETtarget: {kind: script, ref: hello}allowed_authorizers: nullauthz_gates: []override_authorizers: falseoverride_permissions: falseTargets: {kind:script,ref}, {kind:workflow,ref}, {kind:crud,ref,config}, or {kind:websocket,routes:{message_type:script_path},route_key?,message_rate_limit?,log_messages?}. WebSocket defaults route_key:type, log_messages:errors. Changing target kind/binding can require deliberate deletion/recreation; endpoint replacement is not an atomic resource rename.
allowed_authorizers omitted/null inherits; [] denies all, names replace ancestors. authz_gates omitted/null in repo means unmanaged (push omits the gate field); [] declares no local gates but retains ancestors unless override_permissions:true. Both override booleans default false. overrides omitted/null is unmanaged; explicit {} clears overrides. A nonempty retired required_permissions is refused. See endpoint authentication for the different API tri-state (API gate null explicitly inherits).
System login/register nodes are omitted and protected. Proxy mounts are not representable: pull warns/skips, push preserves server proxy nodes. A WebSocket binding to a deleted script cannot serialize and is skipped/protected until repaired in the console. Branch directories do not encode every branch-level contract/auth field; pull is not a lossless dump of arbitrary tree state. Removing a branch can cascade descendants, so inspect before mirror push.
Trigger-file reference
Section titled “Trigger-file reference”Shared ordinary trigger target: {kind:script,name:<qualified path>} or {kind:workflow,name:<qualified path>}. Only schedules additionally support {kind:endpoint,method,path}. Target assets/resources must already exist.
| File | Full top-level fields and defaults |
|---|---|
schedules/*.yaml | Required name, trigger, target; inputs defaults null, enabled:true, on_overlap:skip (`skip |
watches/*.yaml | Required name, source, target; on:[created,modified], filter:{}, timing:{}, inputs:null, enabled:true |
queue-triggers/*.yaml | Required name, resource (AMQP), queue, target; max_concurrent_runs:1, ack_deadline_override:false, inputs:{}, enabled:true |
ws_triggers/*.yaml | Required name, config; enabled:true; config routes map message type to script path, not server script IDs |
im/accounts/*.yaml | Required name, platform, resource; enabled:true, groups:[], meta:null; no credentials field |
im/triggers/*.yaml | Required name, platform, target; enabled:true, optional account, account_groups:[], chat_type:any, optional keyword, require_mention:false, inputs:null, max_concurrent_runs default 4, identity_mode:off, optional identity_key, identity_rule, identity_prompt |
Schedule trigger variants: {kind:cron,cron,timezone?} (IANA timezone); {kind:interval,every_secs}; {kind:once,at} RFC3339. Watches source:{path,recursive:true,max_depth?} uses server absolute path allowed by VIAT_WATCH_ROOTS; filter optional include/exclude lists and min/max size; timing defaults settle_secs:5, reconcile_secs:300, max_fires_per_tick:50, max_concurrent_runs:4, max_entries:20000. WS config defaults: idle_timeout_secs:120, hold:single, max_concurrent:1, queue_capacity:1000, on_overflow:disconnect, log_messages:errors; server validates full nested config. An omitted IM account fans in across accounts of that platform; it is not automatically “the current account”.
name: fixture-intervaltrigger: {kind: interval, every_secs: 3600}target: {kind: script, name: hello}inputs: {name: Reader}enabled: falseThis schedule is deliberately disabled; creating/enabling a trigger can run code without another manual HTTP call. Only resource-backed chat accounts round-trip. Row-secret/QR-bound accounts are left server-side and not removed just because absent. Account credentials stay in resources, not im/ YAML.
Actual push ordering and deletion scope
Section titled “Actual push ordering and deletion scope”viat push is already mirror-mode for the stages that support pruning; there is no push --prune. Stages run sequentially, not in one transaction:
- Metadata/version guards and pending authz steps; roles → authorizers → model; add/widen scope claims.
- Non-library resources, secrets before dependent config resources.
- Scope-claim removals after resource reconciliation.
- Scripts, then workflows.
- Schedules → watches → queue triggers → WS triggers → chat accounts then chat triggers.
- Endpoints, then project crate, then library resources.
Model additions create subjects/types/attrs/actions before rules; pruning reverses dependencies (rules before attrs/actions/types/subjects). Guarded dependencies/delegated ownership may cause reported skips, not a guarantee everything now equals the file. Metadata authorizers/roles may be pruned subject to built-in/reference guards. Scripts/workflows/resources only upsert; deleting their file does not delete server assets.
Schedules/watches/queue-trigger stages skip when their directory is absent, but an existing empty directory mirrors away server items. WS triggers and syncable IM accounts/triggers can prune even when their directories are absent; never use a partial checkout with an all-kind push. Endpoint pruning occurs in the visited tree/parent walks; missing leaf/branch there is deletion, protected exceptions above survive. An entirely unvisited server tree is not a promise of global tree deletion. crate/ absent is unmanaged, present empty clears managed server crate in mirror mode; only allowed source paths are sent (Cargo.toml, lock/build/toolchain files, src/ excluding src/bin/, Rust test/bench/example files); hidden/target/unsupported notes are skipped.
Pull is overwrite, not universal local cleanup
Section titled “Pull is overwrite, not universal local cleanup”Pull writes current remote drafts over represented local paths, replaces viat.yaml, and records authz version. It does not generally delete stale local scripts/workflows/resources/triggers/endpoints: a server-deleted file may remain and be resurrected by a later push. Crate is an exception: pull removes pushable local crate files absent remotely, preserving unsupported notes/build outputs. Do not assume every collection has this pruning behavior.
--only chooses kinds: meta,resources,scripts,workflows,schedules,watches,queue_triggers,ws_triggers,chat,endpoints,crate. It is not a filename filter. Unknown selector text can be silently excluded; spell carefully. Filtered push reports excluded drift and pending publish; --json can follow human stage output and is not a guaranteed clean single-JSON stream.
Authz migration files and push guards
Section titled “Authz migration files and push guards”authz/migrations/<id>.yaml (also .yml) is a nonempty operation list, sorted by filename stem ID; checksum protects applied text. Example unexecuted rename step:
- rename_type: {from: report, to: document}- rename_action: {type: document, from: read, to: view}Supported operations: rename_type:{from,to}, rename_attr:{type,from,to}, rename_action:{type,from,to}, rename_subject_attr:{from,to}, set_origin:{origin,where:in_file|not_in_file}, delete_type:{name,cascade:false}. Keep final viat.yaml consistent. Applied IDs/checksums are not editable history; create a new step for correction. Pending steps run before model mirroring, after projecting their result for deletion guards.
--force-authz overrides “server model changed since this checkout last saw it”; --yes approves model/role deletions (types/attrs/actions/subject attrs/roles), not every destructive sync. Platform-rule-only deletions are ordinary mirror behavior. Version record is local .viat/authz-versions.json, keyed by server/project/canonical checkout; pull/push update it. A partial failed push may already advance the model version and be recorded; it is not a rollback guarantee. Missing authz leaves model unmanaged; explicit empty authz is different.
Export/import differences and recovery
Section titled “Export/import differences and recovery”viat export freshly pulls into staging then packs gzip tar .viatpkg; it is not an archive of arbitrary CWD files and not a database/file-byte/key backup. viat import demo.viatpkg --as cli-demo-copy creates a missing target project, otherwise updates it; it does not change current selection. Default import upserts without pruning, adds scope claims, unions crate files and does not apply checkout model guards/migration steps. --prune explicitly selects supported mirror removals; --update-resources opts into updating existing secret-bearing resources whose connections otherwise remain entirely unchanged. New resources with missing secret placeholders are skipped with a summary (not normal push’s hard failure). Reprovision secrets on the target; skipped/kept items do not prove deploy completeness.
Before push: inspect a complete checkout, selected server/project, diff, stale files, delegated rules and destructive categories. After push: read per-stage/skipped/pending-publish output, run intended DB migration separately, inspect publish plan, then explicitly publish. Failure halfway leaves earlier remote writes/local pull writes applied. Restore from deliberate backup/reconcile; endpoint rollback cannot undo all live settings/model/trigger changes. Clean disposable bundles locally only after inspection; remote deletion/revocation is a separate approved operation. No project DELETE route is registered in this release; do not invent a project-delete cleanup command.
No CLI/network/trigger/migration/provider operation or test was run for this reference. Evidence: crates/viat-cli/src/commands/{sync.rs,bundle.rs}, crates/viat-cli/src/repo/{viat_yaml.rs,resources.rs,endpoints.rs,schedules.rs,watches.rs,queue_triggers.rs,ws_triggers.rs,im.rs,crate_files.rs,authz_steps.rs,model_guard.rs}, API DTO/permission implementations. Older comments say !secret, blanket mirror, and even no project GET; actual code uses $secret, collection-specific pruning, and GET /projects/{slug}. This page follows code, not those comments.