Approvals & E-Signature
Multi-step approval chains, signed documents, and the audit trail around them — built on the approval and document_signature base types, from a real 22-schema clinical trial application.
The Project
app_ctr in ~/supero-test/e2e-tests/apps/app_ctr, namespace trialhub — a clinical trial application with 22 schemas. Its header declares SUPERO_APP_ARCHETYPE: saas and its services: task, approval, notification, workflows, email_smtp, appointment, attachment.ℹ️ Two exemplars, because no one bundle uses both halves
app_ctr, which uses approval:base_approval and no e-signature at all. The signature half comes from app_m71-gym3, whose waiver and waiver_signature extend document_signature:base_document and :base_signature. Both are real; they are simply not in the same bundle.Architecture & Design
{"name": "TrialApproval",
"namespace": "trialhub",
"schema_type": "object",
"parent_type": "tenant",
"extends": "approval:base_approval",
"attributes": [
{"name": "subject", "type": "string", "mandatory": True},
{"name": "approval_type", "type": "string"},
{"name": "requested_at", "type": "string"}],
"references": [
{"name": "Trial", "cardinality": "one",
"back_ref_name": "approvals"}]}
{"name": "TrialApprovalStep",
"namespace": "trialhub",
"schema_type": "object",
"parent_type": "trial_approval",
"extends": "approval:base_approval_step",
"attributes": [
{"name": "approval_uuid", "type": "string", "mandatory": True},
{"name": "step_index", "type": "integer", "mandatory": True},
{"name": "step_notes", "type": "text"}]}parent_type: "trial_approval" on the step. Parenting is how ownership and addressing work — a step is not a free-floating record holding a foreign key.| Your type | Extends | Role in the architecture |
|---|---|---|
TrialApproval | approval:base_approval | The decision itself. subject is mandatory; approval_type and requested_at are the app’s own. |
TrialApprovalStep | approval:base_approval_step | One reviewer’s turn, ordered by step_index, parented to the approval. |
TrialDocument | attachment:base_attachment | Consent forms and lab reports, referencing their Participant. |
TrialTask | task:base_task | Operational follow-up, attached to a Trial. |
TrialNotification | notification:base_notification | Staff alerts, with notification_category and is_read. |
Visit | appointment:base_appointment | A scheduled visit, with actor and resource reference roles. |
waiver *(gym exemplar)* | document_signature:base_document | status (draft, awaiting_signatures, completed, voided, declined), required_signatures, completed_signatures, expires_at, checksum. |
waiver_signature *(gym exemplar)* | document_signature:base_signature | status (pending, signed, declined), signer_uuid, signer_email, signed_at, signature_data, ip_address. |
app_ctr extends nothing: Organization, Trial, Site, Participant, AdverseEvent, plus ten enums including PhaseEnum, ParticipantStatusEnum, SeverityEnum, SeriousnessEnum and OutcomeEnum. Enumerating your vocabulary that precisely is most of what makes a regulated application reviewable.⚠️ extends belongs inside schema_content
attributes, entries {name, type, mandatory} — not JSON Schema.The Flow
- 1
Something needs a decision
A
TrialApprovalis created with asubject, anapproval_typeand a reference to theTrialit concerns. - 2
The chain is laid out
One
TrialApprovalStepper reviewer, parented to the approval and ordered bystep_index. - 3
Reviewers act
Each decision goes through
POST /api/v1/services/executewithservice_id: "approval". There is no approval API of its own — one endpoint serves every service. - 4
Illegal moves are refused
Sixteen services ship state machines. Approving an already-decided step, or reopening a closed approval, returns
409 INVALID_STATE_TRANSITIONrather than a nonsense status. - 5
A document gets signed
A document sits at
awaiting_signatures, countingcompleted_signaturesagainstrequired_signaturesuntil it reachescompleted. - 6
Something material happens
An event binding fires a workflow. In
app_ctrit is on adverse events — exactly the class of thing you never want to depend on a human remembering.
// app_ctr/event_bindings.json
[{"event": "@create:trialhub:adverse_event",
"workflow_id": "adverse_event_alert",
"input_map": {"event_uuid": "uuid",
"event_name": "event_name",
"severity": "severity"}}]@<verb>:<namespace>:<schema>, and input_map maps fields on the created record to workflow inputs. The workflow is a CRUD record of type wf:workflow_definition, each run a workflow_instance and each step a workflow_step_log.What You Get Without Building It
- •The header-and-step structure, with the step parented to the approval so the chain is addressable rather than assembled by query.
- •State machines that refuse illegal transitions with
409 INVALID_STATE_TRANSITIONinstead of corrupting a status field. - •Signature counting:
required_signatures,completed_signatures, and a status that reachescompletedon its own terms. - •Per-step logging —
workflow_instanceandworkflow_step_logare ordinary records you can query, not engine internals. - •Event bindings, so an approval or an incident triggers a workflow at creation without polling.
- •The platform roles, with
fq_name—[domain, project, tenant, record]— as the record’s immutable address. - •Selective publicity:
app_ctrdeclaresPUBLIC_SCHEMAS = ["trial", "site", "landing_override"]— trials and sites public, participants and adverse events not.
💡 Field-level protection
How to Implement in Supero
Model it
- 1
Build the schema
Describe the app in App Studio — it generates the schemas and a reviewable UI — or author it over MCP from your editor. Aim at the object model above: a type opts into a service by extending its base type. Schemas are live on the CRUD API the moment they upload — there is no separate build step to run.
- 2
Configure the services
Imported services arrive uncredentialed. Put the keys for email or SMS notifications in the keystore so approval routing can reach reviewers — the document_signature service itself needs no external key. If the data must stay inside your own infrastructure, this is also the shape you deploy to your own cloud or on-premise — see Deployment.
What you write yourself
⚠️ The routing rules are yours, and so is what a signature means
signature_data and ip_address are fields the base type provides for you to populate — what you write into them, and what legal weight it carries in your jurisdiction, is your determination. The presence of these fields is not a compliance claim by the platform.| Decision | Why it is yours |
|---|---|
| Quorum and delegation | Steps are ordered. Whether three of five suffices, and who may act for whom, is logic you write in the workflow. |
| Escalation on a stalled step | Workflows offer on_error of stop, continue, retry or compensate, with max_retries and timeout_seconds. The policy is your design. |
| Immutability after approval | The platform does not freeze a record because an approval closed. If an approved protocol must not change, enforce it. |
| Identity of the signer | The base type records signer_uuid and signer_email. Verifying the human is your process. |
| Retention, and what the trail contains | You get a per-step log. How long it is kept, and whether its fields satisfy your regulator, are not platform settings. |
app_ctr imports email_smtp rather than SendGrid — the usual choice when notifications must leave through infrastructure you control. Both configure the same way, with credentials in the keystore.Deployment
- 1
Preview while you iterate
A preview deploy answers at
https://preview-{domain}-{project}.supero.livefor a short lease while you build and test. Publish Your App covers how preview and live differ. - 2
Go Live
Go Live publishes to a permanent, Supero-hosted URL —
https://live-{domain}-{project}.supero.live— with no cloud account to connect and nothing to provision. It is asynchronous: you get a202and a poll URL, then the live address. The full contract is in Publish Your App. - 3
Watch the logs
When a deploy fails or the live app misbehaves, the **Cloud Deploy Logs** page in the dashboard shows the published service’s container logs — pick the Live lane to read the published app rather than the preview build (up to ~24 hours back, ~500 lines per request). For platform activity — requests, auth, errors across your domain —
GET /api/v1/logsis a filtered read, and a single request traces across services at/api/v1/logs/trace/{correlation_id}when you set your ownX-Correlation-ID. See Operations & Observability.
Final Thoughts
extends must sit inside schema_content. At the wrapper level it is silently stripped on upload — the schema saves without error, and you are left with a plain object type that has no approval behaviour and no state machine.Where to Go Next
The automation
Workflow Engine Reference
Step types, on_error, retries and event bindings in detail. Open
Who may act
Roles & Permissions
The platform roles, and how to scope reviewers to what they may decide. Open
Protecting fields
Security
Encryption, access policies and what the platform does and does not assert. Open
On this page