Pipeline apps

Collaborative board walkthrough

A developer-preview portable pipeline app spanning release distribution, rendering, durable replay, and causal entity merge.
This is the golden developer-preview fixture for the portable Pipeline Apps contract. It covers strict authoring, managed signed publication, tenant installation, verified remote loading, the shared Vue host, durable Rust/WASM replay, schema-descriptor-driven board mutations, and schema-descriptor-bound causal entity merge, owner/grantee sharing, and operator-reviewed catalog publication. Arbitrary schema-descriptor-generated subscriptions, custom client transforms, and public self-service publication remain planned.

The collaborative board is deliberately ordinary: columns contain cards, cards have labels and comments, several people can edit, and the application works on web and mobile. That makes it a useful proof of the complete contract. It exercises typed storage, reads, writes, subscriptions, page layout, local state, conflict resolution, permissions, installation, and upgrades without requiring a separate service architecture.

Target experience

  1. A user installs a pinned board release into a workspace.
  2. The shared client renders the release's board and card-detail surfaces.
  3. Queries resolve through exact named pipeline revisions; the foreground host refreshes declared entity collections from authorized snapshots.
  4. Edits become typed operations and enter the durable device queue.
  5. Offline operations replay after reconnect or app resume.
  6. The server authorizes operations, merges them, and materializes cheap read datasets.
  7. Other visible, online clients refresh the converged server materialization.
  8. An upgrade displays schema, pipeline, surface, dependency, and permission changes before moving the installation pin.

Artifact map

ArtifactBoard responsibilityStatus
Rust serde schemas and runtime descriptorsBoard, Column, Card, and Comment recordsExact schema-descriptor and dataset pins are validated and registered with installed releases
Pipeline manifestsStrict row-shape list/write/move/comment/event fixturesResolved to exact PipelineSpec hashes with declared capability checks
Custom transformDomain-specific move validation or derived activity rowsTrusted native server transforms current; portable client UDF planned
Entity operation policyField clocks, sets, ordered references, and tombstonesDescriptor-checked causal reducer with durable dots/version vectors and server materialization
Surface treeBoard and card-detail pages with bindings and actionsVerified installed-package source and shared Vue/Capacitor host with create/edit/move/comment/label adapters
Application releaseExact membership, hashes, capabilities, and provenanceLocal unsigned candidates plus managed signing, strict verification, receipt, and immutable registry storage
InstallationTenant pin, accepted permissions, and upgrade historyTenant-scoped install, consent, upgrade, rollback, revocation projection, and write enforcement

Try the local release path

From the repository root:

rac app validate apps/collaborative-board/app.toml
rac app build apps/collaborative-board/app.toml
rac app inspect apps/collaborative-board/build/app-release.pb

Validation reports valid=true for the local application graph. Building strictly parses all six referenced pipeline files, resolves their canonical PipelineSpec hashes, and writes deterministic CBOR and JSON artifacts. The CLI retains the app-release.pb filename for compatibility; the local candidate payload is deterministic CBOR, with a JSON companion. Inspection reports publish-eligible=false because the local fixture has no registry-resolved schema-descriptor/dataset proofs or publisher signature. Those warnings are part of the publication boundary rather than skipped checks.

1. Define records

The card entity in apps/collaborative-board/app.toml declares its fields directly, each with the merge policy that governs it:

[[entities]]
id = "card"
type_url = "app_example.collaborative_board.Card"
dataset = "app_example.collaborative_board.Card"
identity_fields = [1]
display_fields = [3]

[[entities.fields]]
tag = 1
name = "id"
kind = "string"
cardinality = "required"
merge = "immutable"

[[entities.fields]]
tag = 2
name = "column_id"
kind = "entity_reference"
cardinality = "required"
references = "column"
merge = "last_writer_wins"

The remaining fields follow the same shape: title (searchable, max length 200), description, labels (repeated, add_wins_set, tombstone-retaining), assignee_id, and archived (bool, defaulting to false). Field tags are stable identity for the merge reducer, so a field keeps its tag across revisions.

The final stored records also follow the platform's owner scope and primary key conventions. Schema registration gives each type URL an exact schema hash. In this walkthrough, a descriptor is the runtime metadata derived from that checked-in Rust serde schema: field tags, kinds, identity fields, and merge policy. It is runtime schema metadata, rather than a code-generation artifact. Surface fields and transforms refer to this registered schema metadata rather than recreating a TypeScript-only model.

The merge policy belongs beside the schema contract:

FieldPolicyReason
title, column_id, assignee_idHLC-ordered LWW registerOne current scalar value
labelsCausal add-wins setConcurrent add/remove events retain their causal frontiers before policy resolution
commentsIndependent immutable recordsChild operations participate in durable ordered replay
deleted stateClocked tombstoneOld updates cannot revive a deleted card
descriptionLWW initially; text CRDT opt-inCharacter-level merging adds cost and should be explicit

The entity merge crate validates manifest field declarations against an exact pinned schema-descriptor set. Its preview reducer applies immutable and LWW registers, causal add/remove sets, ordered references, and delete/update frontiers to dynamic messages. Installation-qualified client sites allocate durable dots; the DES oplog validates dot collisions against the exact tenant, entity reference, and canonical payload before materializing the result.

2. Declare server pipelines

The current manifest path can express the board's backend routes as small graphs:

  • board-list reads active and archived rows with nonempty ids, columns and titles, excluding legacy incomplete records from hydration without deleting them. It sorts active cards first and then by column_id and title, and projects the card fields. The Board and Archive surfaces select their display from the same bounded collection;
  • board-upsert-card trims titles, rejects empty ids/titles, and projects a complete row into the card dataset;
  • board-move-card rejects empty card/column ids and projects a complete card row;
  • board-add-comment trims bodies, rejects empty ids/card ids/bodies, and projects a comment row;
  • board-comments reads workspace-authorized comments in creation order; the detail list filters by the selected card, and hydration merges durable pending creates into the same fixed collection used by optimistic writes;
  • board-events subscribes to non-archived card rows with a bounded projection.

These are executable row-shape manifests. Portable entity writes use the installation-bound CBOR EntityOp transport and require an active, verified DATASET_WRITE grant for the release-pinned dataset. The schema-descriptor-driven action adapter covers card creation and editing, moves, label-set membership, and comment creation. A future custom transform could handle a rule such as “a closed card cannot move into an active sprint without reopening,” with pinned input/output schemas and an exact artifact hash.

The public pipeline manifest preview covers the referenced execution graphs. The application compiler separately parses the outer manifest and resolves every source path through that strict pipeline parser.

3. Group a release

The checked-in fixture uses accepted application TOML. This is a shortened excerpt from apps/collaborative-board/app.toml:

[app]
id = "collaborative-board"
name = "Collaborative Board"
version = "0.1.3"
minimum_runtime_version = "1.0.0"
default_view = "board"

[[entities]]
id = "card"
type_url = "app_example.collaborative_board.Card"
dataset = "app_example.collaborative_board.Card"

[[pipelines]]
name = "board-list"
source = "pipelines/board-list.toml"

[[views]]
id = "board"
name = "Board"
initial_page = "board"

rac app build apps/collaborative-board/app.toml resolves every pipeline, computes the publisher-independent canonical package hash, and writes deterministic CBOR and JSON candidates under the fixture's ignored build/ directory. The local candidate is deliberately unsigned. Managed publication binds the exact authored source and normalized manifest to that digest, rejects unknown field tags, verifies the separate publisher attestation, stores the immutable release, and returns a signed verification receipt.

4. Describe the board surface

Surface declarations live in the same accepted manifest:

[[views.pages]]
id = "board"
route = "/"
title = "Collaborative Board"

[views.pages.root]
id = "board-shell"
type = "stack"

[[views.pages.root.children]]
id = "board-columns"
type = "board"

The component and action names come from an allowlisted registry. The shared validator checks v1 route, component, action-target, binding-shape, execution site, capability, and schema-descriptor rules. The current adapter derives safe create, edit, move, label, and comment operations from signed entity schema descriptors; arbitrary schema-descriptor-generated live subscriptions remain planned.

Card lifecycle

The create form accepts a title and description. Card edits, moves and label changes update the selected entity after the durable operation succeeds, so the next action uses the resulting values. Archive and restore change only the existing archived field through the same offline-capable entity path; the card and its comments remain stored. The Archive page opens archived cards for restoration to their previous column.

The board component accepts a static archiveFilter prop with active, archived, or all. Omission preserves the existing all behavior. Filtering and display projection run in the shared Rust core/WASM, before the per-column display limit. Display excerpts never replace the full entity carried by a selection action, including long descriptions, labels and identifiers. Cached archived records can therefore remain available for offline restoration without appearing on the active board.

This remains a bounded preview: board-list reads at most 500 cards, and active cards sort before archived cards within that window. The Archive page does not provide pagination or promise a complete history beyond that limit. Version 0.1.3 must be published after deploying the host and publication validator that support archiveFilter; existing installations must upgrade their signed pin. Runtime version 1 alone does not negotiate this additive component property.

5. Execute on web and mobile

The canonical Vue application already builds into the Capacitor Android and iOS shells. The local fixture and remote installed-package source run through that shared Vue code. Remote loading resolves the authenticated tenant installation, verifies the immutable receipt and signed mutable installation projection, checks the authenticated response sequence, requires an active installation, and then projects the release:

AppRelease
  -> verify hashes and signature
  -> register schema descriptors and components
  -> resolve installed capabilities
  -> build routes and surfaces
  -> open query/subscription bindings
  -> render with the shared Vue host

Native actions such as notifications, camera input, file selection, or location will use separately declared host capabilities. Arbitrary schema-descriptor-generated subscriptions and native effect adapters are still planned.

6. Work offline and converge

The portable Rust/WASM runtime binds state to the installation, tenant, user, and an installation-qualified device site. It persists one bounded canonical CBOR envelope containing HLC state, entity cache, causal context, queue sequence, retry metadata, acknowledgements, dead letters, and verified package trust state through Capacitor Preferences. Mutations reload the latest durable snapshot under a cross-tab lock, and reconnect/resume hooks replay the ordered queue through the installation-bound CBOR entity endpoint.

An offline move follows this path:

  1. Validate the proposed Card change against the pinned Rust schema descriptor.
  2. Create a typed operation with an id, site id, and HLC stamp.
  3. Persist the operation before exposing the queued result.
  4. Replay after reconnect in deterministic order.
  5. Record an applied, duplicate, retry, or permanent-rejection disposition.
  6. Merge accepted operations and any concurrent remote operations using the field's declared policy.
  7. Refresh the rendered board through the signed query and entity-read interfaces.

Authentication and dataset grants remain server-authoritative. Optimistic state can be rejected, in which case the client records the error and re-materializes from acknowledged operations.

Foreground synchronization

The signed release opts into foreground refresh with root config runtime_entity_refresh = "foreground_snapshot_v1". This is the author's declaration that all entity hydration pipelines are read-only and safe to repeat. The board's card and comment queries satisfy that contract. The execution API does not enforce a read-only mode, so existing releases without the opt-in keep startup-only hydration and must be explicitly upgraded to enable refresh.

The installed host refreshes the opted-in collections declared by signed entity hydration actions, including cards and comments. It runs at most one refresh pass at a time, with at least 15 seconds between starts while the app is visible and online. Reconnect, returning to the app, replay completion and settled local actions request a refresh through the same bounded scheduler. Failures retain the current display, show an error and retry with backoff up to two minutes.

Each pass invokes exact release-pinned pipelines with the installation identity, then reads authorized entity values and folds pending durable operations. A local action or installation/workspace change invalidates an overlapping display result. Only declared collections are replaced; drafts, selected-card form values and navigation remain local. Open another card to load its refreshed values into the editor. Hidden and offline clients refresh after returning online and visible.

This is periodic snapshot convergence, not a live subscription guarantee. The server does not yet accept installed application specs on its subscription endpoint; the board-events fixture alone does not create a browser subscription. Refresh retains the existing bounded query windows, so it does not establish pagination, arbitrary board/column editing or native background synchronization. Two independent browser contexts can verify this shared host behavior; native device conformance still requires its own device run.

7. Share, upgrade, and fork

  • Share the application: grant a principal access to the private catalog entry; the grantee sees it under Shared with me and can install the pinned release into an authorized workspace.
  • Share board data: create a separate dataset grant, potentially read-only.
  • Upgrade: inspect changes to schemas, pipelines, surfaces, dependencies, migrations, and capabilities, then move the installation pin.
  • Rollback: return to a compatible prior release while preserving the append-only operation history.
  • Fork: create a new app identity and release lineage. Source data stays in place unless the user explicitly copies or grants it.

Preview verification checklist

The developer-preview implementation exposes each lifecycle boundary for targeted verification. A deployment or device-conformance result should be claimed only after its corresponding check has run:

  • publish and install a signed release, then load it without a hard-coded app route;
  • render the same installed surface through the shared web and Capacitor host;
  • inspect every exact schema, pipeline, artifact, surface, and dependency pin;
  • replay concurrent offline edits from installation-qualified sites and inspect the converged materialization;
  • reject an undeclared capability, inactive installation, and tampered bundle;
  • inspect an upgrade diff, preserve pending operations across the rebind, and exercise rollback.

Continue with release and security lifecycle or return to the Pipeline apps mental model.

Comment visibility and bounds

Comment display uses the generic list filterField and bound filterEquals properties. Exact matching happens before the display limit; an empty or missing selection renders no rows. This display filter does not authorize data access: installation grants and workspace scope remain enforced by the server. The reference query reads at most 200 comments and the selected-card list shows at most 100. Pagination and live comment subscriptions remain future work.

Causal list contract

Ordered reference fields use unique, nonempty string members and causal snapshot frontiers. A snapshot replaces only snapshots its causal context observes. Concurrent snapshots retain their union: traverse maximal snapshots in ascending (site id, counter) order and append each member at its first occurrence. This resolves conflicting reorders deterministically without using wall clocks. An observed later snapshot can reorder or remove any member. A concurrent snapshot that still contains a removed member retains it until a later edit observes both. This is a snapshot sequence contract; it does not promise character-level editing or positional insertion intent across concurrent reorders.

The server persists the maximal snapshots with the entity clock rollup; the portable runtime retains this rollup and uses its version vector on the next queued operation. Portable list and set mutations require causal dots. The current board surfaces mutate cards and comments; arbitrary list-editing controls are not yet exposed by the host action contract.

Publication requires a trusted signed receipt naming redgold.causal-snapshot-lists-add-wins-sets.v1, bound to the exact release. The verifier first compiles every entity with the installed-write descriptor mapping. Missing or unknown attestations and unsupported merge strategies remain ineligible. Local unsigned candidates cannot assert their own compatibility.

For UI deployment evidence, read the redgold-ui-revision comment in the served HTML index. For UI prefixes, the proxy derives /build-revision.txt from that same index stamp rather than the legacy storage object. The endpoint is non-cacheable and fails closed when the index has no valid revision; docs prefixes retain their separate marker protocol. Compare the marker with the loaded application and service worker when verifying browser delivery.

© 2026 Redgold Labs