Data Architecture
This section defines the system’s data domains, the authoritative sources of truth, and the contracts that keep reads simple and writes safe. The platform is intentionally web3‑first: economic and access state lives on‑chain; media and catalogs live on decentralized storage and edge delivery; identity leverages wallets and zkLogin without centralizing user profiles. The result is a model that is both verifiable and resilient, while remaining straightforward for clients to query.
Data domains at a glance
- On‑chain economic and access state on Sui (Move): content licensing, access entitlements, bounty escrow, and reputation, plus anchoring for oracle receipts and market resolutions.
- Off‑chain media and catalogs on Walrus and CDN: immutable media blobs, encrypted submission packages, and static catalogs/manifests for discovery.
- Identity and session context: wallet addresses and zkLogin principals mapped client‑side to active accounts; sponsored transaction metadata limited to what is needed for abuse control.
- Telemetry and audit: privacy‑preserving client telemetry and on‑chain event streams; no doxable PII recorded on‑chain.
On‑chain entity model
The following ER diagram captures core objects and their relationships. Invariants are enforced by Move modules and verified client‑side.
Move Object Definitions
module pml::data_types {
use sui::table::{Self, Table};
use sui::balance::{Self, Balance};
use sui::dynamic_field as df;
use sui::dynamic_object_field as dof;
/// Content object with proper abilities
struct ContentObject has key, store {
id: UID,
kiosk_policy_id: ID,
walrus_blob_id: vector<u8>,
content_hash: vector<u8>,
license_meta: vector<u8>,
author: address,
co_authors: vector<address>,
created_at: u64
}
/// Entitlement with key ability only (non-transferable)
struct Entitlement has key {
id: UID,
owner: address,
content_id: ID,
kind: u8, // 0: article, 1: subscription
valid_from: u64,
valid_until: u64,
tier: u8
}
/// Shared bounty object
struct Bounty has key {
id: UID,
owner: address,
amount: Balance<SUI>,
source_deposit: Balance<SUI>,
deadline: u64,
terms_hash: vector<u8>,
status: u8,
submissions: Table<address, Submission>
}
/// Submission stored in bounty table
struct Submission has store {
source_id: address,
encrypted_package_hash: vector<u8>,
walrus_blob_id: vector<u8>,
seal_policy: vector<u8>,
timestamp: u64
}
/// Reputation with dynamic fields for extensibility
struct Reputation has key {
id: UID,
score: u64,
model_version: u8
}
/// Market for prediction/resolution
struct Market has key {
id: UID,
category: vector<u8>,
terms_hash: vector<u8>,
resolution: Option<ResolutionReceipt>
}
/// Resolution receipt with oracle proof
struct ResolutionReceipt has store, drop {
market_id: ID,
resolution: vector<u8>,
oracle_receipt_hash: vector<u8>,
timestamp: u64
}
/// Oracle receipt for verification
struct OracleReceipt has store, copy, drop {
enclave_measurement: vector<u8>,
zktls_proof_hash: vector<u8>,
payload_hash: vector<u8>,
attestation: vector<u8>
}
}
Dynamic Fields for Extensibility
module pml::extensions {
use sui::dynamic_field as df;
use sui::dynamic_object_field as dof;
/// Add metadata to content without changing struct
public fun add_content_metadata(
content: &mut ContentObject,
key: vector<u8>,
value: vector<u8>
) {
df::add(&mut content.id, key, value);
}
/// Add analytics object to content
public fun attach_analytics(
content: &mut ContentObject,
analytics: Analytics
) {
dof::add(&mut content.id, b"analytics", analytics);
}
struct Analytics has store {
views: u64,
unique_readers: u64,
avg_read_time: u64,
ratings: vector<u8>
}
}
Design notes
- Addresses are shown as primary keys to reflect Sui’s object identity model. Where helpful, logical keys (like
content_hash) provide integrity assertions verified by clients. ENTITLEMENTobjects are designed for O(1) verification by the frontends: ownership and validity windows can be checked with a minimal set of reads.SUBMISSIONstores only hashes and references; the encrypted package itself is always off‑chain, preserving confidentiality.- Oracle receipts and market resolution are first‑class to support editorially curated markets with verifiable outcomes.
Off‑chain media, catalogs, and identity mapping
Media and catalogs are immutable where possible. Clients rely on hashes and blob identifiers embedded in on‑chain objects to validate integrity.
Identity mapping remains client‑local. Wallet addresses and zkLogin principals are mapped in memory and secure storage on the device; no centralized profile store is required to authorize reads.
Eventing and client‑side indexes
Move modules emit events that clients can subscribe to via RPC to maintain lightweight indexes and caches. These client‑side indexes accelerate UX without becoming sources of truth.
Indexing principles
- Determinism: indexes are purely derived from on‑chain events and content manifests; no hidden business logic.
- Idempotence: reprocessing the same event stream yields the same index; caches are safe to drop and rebuild.
- Privacy: cached values are keyed by non‑PII identifiers; sensitive materials are never cached in plaintext.
Data lifecycle and retention
- On‑chain: economic state, access entitlements, bounty outcomes, and receipt anchors are permanent and append‑only; upgrades are handled by module versioning and data migration procedures that preserve replayability.
- Off‑chain: published media is immutable; encrypted packages for submissions are retained per policy and can be evicted after resolution where allowed; catalogs are versioned manifests to support rollback and cache busting.
- Client: indexes and caches are ephemeral and re‑derivable; sensitive keys and session tokens follow least‑privilege handling and platform‑level secure storage guidance.
User-Specific Data Models
The platform maintains separate data models for each user type, ensuring clear boundaries and appropriate privacy levels.
Reader Data Model
Journalist Data Model
Source Data Model
Query patterns
Reader Queries
- Check Access:
entitlement.reader == address AND entitlement.valid_until > now() AND entitlement.content_id == requested - List Subscriptions:
subscription.reader == address AND subscription.end_date > now() - Content Discovery: Read catalog → filter by preferences (client-side) → check entitlements → display
Journalist Queries
- Revenue Dashboard:
SUM(revenue_stream.amount) WHERE journalist == address AND timestamp > period_start - Content Performance:
COUNT(entitlement) WHERE content.journalist == address GROUP BY content_id - Bounty Management:
bounty.journalist == address AND bounty.status IN ['active', 'pending_review']
Source Queries
- Bounty Browse:
bounty.status == 'active' AND bounty.deadline > now()(anonymous access) - Submission Status:
submission.source_id == ephemeral_id AND submission.bounty_id == target - Reputation Check:
reputation.anonymous_id == source_id(never reveals identity)