ArchitectureSecurity →
Docs

Architecture

How Plinth stores forms and submissions, where the trust boundaries fall, and what the browser, Sui, Walrus, and Seal each do.

Layers

The browser is the only authority that reads, encrypts, and submits. The serverless routes provided by Next.js are limited to thin proxies for CORS and rate friendly aggregator reads. No Plinth managed database stores form data.

            Browser
              |
     Next.js App Router
              |
   +----------+----------+
   |          |          |
 Sui RPC   Walrus      Seal
 (mainnet) (HTTP)   (key servers)
   |          |          |
   +----------+----------+
              |
        Sui mainnet
        Walrus storage

Onchain data model

The Move package plinth defines four modules.

form_registry

A single shared FormRegistry object indexes every form by slug. Each entry stores the form owner, the schema blob ID on Walrus, the schema version, the admin allowlist address pointer, and the creation epoch. The registry is shared, but only the form owner can update or retire a form.

public struct Form has key {
    id: UID,
    slug: String,
    owner: address,
    schema_blob_id: String,
    schema_version: u64,
    admin_allowlist_id: ID,
    created_at_epoch: u64,
    retired: bool,
}

submission

Each submission is its own Sui object owned by the submitter address. It records the parent form ID, the submission blob ID on Walrus, the encryption envelope ID, the submitted at epoch, and a revoked flag. The submitter can call revoke to mark the submission tombstoned. The form owner can call prioritize, note, and set_status only against the index, not against the submission object itself, so submitter ownership is preserved.

public struct Submission has key, store {
    id: UID,
    form_id: ID,
    submitter: address,
    blob_id: String,
    seal_envelope_id: String,
    submitted_at_epoch: u64,
    revoked: bool,
}

admin

AdminAllowlist is a shared object listing wallet addresses that can decrypt the sensitive fields of a form. The form owner holds an AdminCap and can add or remove entries. A second cap, OwnerCap, can transfer ownership of the form to a different wallet. Both caps are independent objects so they can be rotated or held in cold storage.

events

Every public action emits an event so indexers and aggregators can build read views without polling.

EventEmitted on
FormCreatedNew form published
FormUpdatedSchema or admin allowlist changed
FormRetiredForm taken offline
SubmissionCreatedNew submission written
SubmissionRevokedSubmitter revokes their submission
SubmissionAnnotatedAdmin sets note, priority, or status

Walrus blob layout

Schema blob

{
  "schemaVersion": 1,
  "name": "Bug reports for Plinth",
  "description": "Report a bug. Plain language is fine.",
  "fields": [
    {
      "id": "title",
      "type": "rich_text",
      "label": "Short summary",
      "required": true,
      "encrypted": false
    },
    {
      "id": "severity",
      "type": "dropdown",
      "label": "Severity",
      "required": true,
      "options": ["low", "medium", "high"],
      "encrypted": false
    },
    {
      "id": "wallet_email",
      "type": "rich_text",
      "label": "Contact email",
      "required": false,
      "encrypted": true
    }
  ],
  "adminAllowlist": [
    "0xdd9ae8b9038f1149eb3791ac0a698105bb248af883f2c684f9d8f837f71fd2da"
  ]
}

Submission blob

{
  "schemaVersion": 1,
  "formId": "0x...",
  "submitter": "0x...",
  "submittedAtEpoch": 142,
  "fields": {
    "title": "Form reordering breaks on Safari",
    "severity": "high",
    "wallet_email": {
      "ciphertext": "base64",
      "envelopeId": "seal-...",
      "policy": "form-admin-allowlist"
    }
  }
}

Encrypted fields carry a Seal envelope identifier that resolves to a key policy. The key policy is enforced by Seal key servers and requires the requesting wallet to be on the form admin allowlist.

Seal key hierarchy

        Form key policy
              |
   resolves to allowlist of
              |
    +---------+---------+
    |                   |
 Form owner       Admin allowlist
    |                   |
 OwnerCap         AdminCap holders

When a public submitter encrypts a sensitive field, the client retrieves the public key bundle for the form policy from Seal and encrypts client side. When an admin wants to read, the client requests a session key from Seal key servers, signs a wallet challenge, and retrieves the data key only if the wallet is on the form allowlist and the policy has not been revoked.

Trust boundaries

BoundaryTrusted to
Browser of submitterEncrypt before upload, sign Sui transaction
Sui mainnet validatorsHonor object ownership and allowlist updates
Walrus storage nodesPersist blob, return blob on read

For the full threat model and capability semantics, see the Security page.