Architecture Decisions

When to use what — practical guidance for designing your DUAL application.

Single vs Multiple Templates

One of the first architectural decisions is whether to use a single template for all asset types or separate templates for each. The rule of thumb: if the properties differ significantly between asset types, use separate templates. If only the values differ, use one template.

For example, if you're tokenizing real estate, you might have properties like bedrooms, sqft, and price that are universal. In this case, one template works well. But if you also want to tokenize vehicles with properties like engine type and VIN, those are fundamentally different — use a separate template.

Benefits of separate templates: clearer schema separation, easier validation, simpler action definitions, better indexing. Benefits of a single template: less setup, simpler client logic, unified querying.

Event Bus vs Webhooks

The Event Bus and webhooks serve different purposes in your architecture. Use the Event Bus to execute actions (write operations) and webhooks to receive notifications about what happened (read operations).

Event Bus is for triggering state changes: minting an object, transferring ownership, updating properties. Webhooks are for reacting to those changes: notifying a user when they received an asset, syncing to an external database, triggering a workflow.

In practice, your architecture flows like this: Client → Event Bus (write) → Sequencer → State change → Webhook (notification) → Your backend (read/react). Never use webhooks for critical state changes — they're best-effort.

Roles & Permission Design

Structure your organization's roles to balance security and usability. Start simple: 3 base roles: Admin (full access), Operator (can mint/transfer objects), Viewer (read-only access).

Admin has unrestricted access to templates, API keys, billing, and members. Operators can execute actions on objects and view templates but cannot modify template definitions or manage API keys. Viewers can only see objects and historical data.

As your team grows, add custom roles: QA (can mint test objects), Finance (can view billing but not change), Support (can view objects and activity logs). Use the custom roles API: POST /organizations/{id}/roles.

Pro tip: Rotate API keys quarterly and use different keys for different environments (dev/staging/prod). Limit API key scope to specific templates or actions when possible.

On-chain vs Off-chain Storage

Understanding what lives on-chain vs off-chain is critical for cost and performance. Object properties are stored off-chain (fast, mutable). The Sequencer batches and anchors only fingerprints on-chain (immutable proof).

Use off-chain storage (DUAL's default) for metadata, properties, and mutable state: addresses, current owner, status, timestamp. Use on-chain anchoring for immutable proofs: ownership history, transaction audits, fraud challenge windows.

Cost implication: storing 1 million properties off-chain costs ~$10k/month in DUAL storage fees. Anchoring 1 million objects on-chain (in batches) costs ~$100/day in gas. Most use cases benefit from hybrid: keep properties off-chain, anchor critical ownership events on-chain.

Direct API vs SDK vs CLI

Three ways to interact with DUAL, each suited to different scenarios:

  • SDK (@dual/sdk) — Best for production applications. TypeScript library with type safety, request signing, error handling, and retry logic. Use this for web apps, mobile backends, and services.
  • CLI (@dual/cli) — Best for testing, scripting, and one-off operations. Use it for bulk minting from CSV, local development, debugging, and CI/CD pipelines.
  • Direct API (HTTP REST) — Use this when you need fine-grained control, work in a non-TypeScript language (Python, Go, Rust), or want to integrate with existing HTTP client libraries.

Production rule: Always use the SDK. It handles request signing and has battle-tested error handling. Only use direct API if you have a specific reason (language constraint, legacy integration).

Template Versioning Strategy

Templates with live objects can never be modified. Plan for versioning from day one. Name templates with a version suffix: my-org::property::v1, my-org::property::v2.

When you need to add a field or change validation, create a new template version. Migrate existing objects to the new template with a batch action. Never try to modify a template with live objects — it will fail.

Strategy: Use v1 for MVP, v2 when you need schema changes. Keep the old template around for 3 months so clients can still query old objects. Add a deprecation notice in your docs.

Example flow: Create property::v1, mint 1000 objects. Need to add square footage field? Create property::v2, write a migration script that creates new objects with both old and new data, archive v1.