Veydrin Community Network
The technical standard governing how VCN-enabled applications discover each other, sign and route data packets, synchronize state across devices, and operate without any central server. Built for the world — starting where infrastructure is scarce.
The VCN Protocol governs the technical layer of the Veydrin Community Network — how devices running VCN-enabled applications find each other, exchange signed data packets, and form a regional mesh without any server that the Veydrin Order operates. The one external infrastructure dependency is the Tier 3 bootstrap endpoint (Cloudflare Workers + KV), used only during early regional adoption before the peer mesh fills in. Its source is published and it can be self-hosted by any community.
The protocol has two concerns addressed in separate sections:
This spec governs software behavior. The organizational governance layer — the nine rules, node federation, trust badge, and enforcement framework — is a separate document: see the VCN Community Spec.
All VCN member applications must implement VODS v1.0 as the data interoperability baseline. VODS defines the document envelope, privacy floor, and export schema that VCN packets carry as payloads. The VCN Protocol defines how those packets move. They are siblings — neither depends on the other, but both are required for full compliance.
On first launch of any VCN-enabled application, the vcn_node package generates an Ed25519 key pair locally on the device. No network connection is required or used. No server is involved at any point.
The node ID is the Base64-URL-encoded Ed25519 public key, 44 characters in length without padding. This is the key itself — no hash, no truncation, no information loss.
node_id ::= base64url( ed25519_public_key ) example ::= "X9kLm2vQpR7wN4yZ1cFbDhEaG8sT0uIo3jK6xV5nMeA"
Every packet emitted by a node is signed with that node's Ed25519 private key. The signature covers the canonical JSON serialization of the packet body (all fields except the signature field itself). Recipients verify the signature before processing any packet. Packets that fail signature verification are silently discarded — no error is surfaced to the user.
A user may export their node identity to migrate to a new device or back up their keys. The export format is a JSON file encrypted with a user-chosen passphrase using AES-256-GCM:
{
"vcn_identity_version": "1.0",
"node_id": "Base64-URL public key",
"private_key_enc": "AES-256-GCM encrypted private key (Base64-URL)",
"nonce": "12-byte GCM nonce (Base64-URL)",
"salt": "16-byte KDF salt (Base64-URL)",
"kdf": "PBKDF2-SHA512",
"kdf_iterations": 310000,
"created_at": unix_ms
}
The passphrase is converted to a 256-bit AES key using PBKDF2-SHA512 with the following parameters:
The passphrase is never stored or transmitted. Import requires the correct passphrase. The existing key pair is replaced on import — apps MUST warn users before overwriting an existing identity. Implementations SHOULD enforce a minimum passphrase length of 12 characters.
The source_app envelope field identifies the originating application. The following identifiers are registered for v1.0. Third-party VCN implementations must use a distinct identifier not listed here:
| source_app value | Application |
|---|---|
eluvorim | Eluvorim — Community Ledger |
apiara | Apiara — Beekeeping Management |
chinara | Chinara — Soilless Cultivation |
numaka | Numaka — Permaculture |
arcalox | Arcalox — Personal Resilience Logging |
vcn | VCN infrastructure packets (bulletin, update) |
source_app is set at compile time as a package constant in each consuming app's vcn_node configuration. It is not user-configurable.
Added in VCN Protocol v1.1.
When a user creates an organization within a VCN application, a separate Ed25519 keypair MUST be generated for org ownership. This keypair is independent from the node identity defined in §2. The org founder key is used exclusively to sign org-scoped broadcasts. It MUST NOT be used for community packets, messages, or any other purpose.
If the org founder key were the same as the node identity, any observer could correlate org broadcasts with the founder's pseudonymous community activity. Separating the keys ensures that knowing who leads an organization does not deanonymize their personal mesh activity.
founder_node.org_signature field in the payload, computed by signing the broadcast title with the org founder private key.org_signature against the stored founder_node public key.| Field | Type | Req? | Description |
|---|---|---|---|
org_scope | string | req | Org slug. Lowercase ASCII, digits, hyphens only. Max 64 characters. |
org_name | string | opt | Human-readable org name for display. Max 128 characters. |
org_signature | string | req | Base64-URL Ed25519 signature (88 characters) of org_scope + : + broadcast title, signed with the org founder private key. If the title is empty, sign org_scope alone. |
Receiving nodes MUST verify org_signature against the stored org founder public key before displaying the broadcast. If verification fails, the packet MUST be discarded silently. If the receiving node has no stored founder key for the given org_scope (not a member), the packet MUST be ignored.
Organizations share membership via QR code. The QR data MUST be a JSON object:
{
"type": "vcn_org",
"slug": "honey-for-humanity",
"name": "Honey for Humanity",
"founder_node": "Base64-URL org founder public key"
}
Scanning this QR code MUST create a local org membership with the provided slug, name, and founder key. The founder_node field contains the org founder PUBLIC key (not the VCN node_id), enabling broadcast verification. QR data MUST NOT exceed 2 KB.
Org founder public key: InjrkeV51Aox9zEKu61feig4GKFo9vy_tiN-gxPwe_k
Vector 1: Sign honey-for-humanity:Meeting this Saturday
Signature: HkhLse1ZtInb45yOMnlvcUwcUYE4rgF8Y39NbM2H5GK_3BWZnmPvVgbdNyBQQboPtkz-6K1bAGSlv9QMONDCAA
Vector 2 (empty title): Sign honey-for-humanity
Signature: 3wW0eCsWEyvPUl01AbGyTN5P4vfCv81Ibi6_HmXJ6wnMwbDvOW7-41Rx6B0ifrkr51wSCjxvaP91EwEMSML4Bw
Any correct Ed25519 implementation given the public key above MUST successfully verify both signatures.
Key compromise: If the org founder private key is compromised, the attacker can send verified broadcasts to all members. There is no revocation mechanism. The org founder SHOULD create a new org with a new keypair and redistribute the QR code. The old org slug becomes untrusted.
Spoofing: Any node can include org_scope in a packet payload. Without a valid org_signature, receiving nodes discard it. The signature is the proof, not the field.
Added in VCN Protocol v1.1.
VCN uses the area_tag field as a routing channel. In addition to geographic area tags, VCN defines the following special channel patterns:
| Pattern | Purpose | Scope |
|---|---|---|
{area_tag} | Local community packets (bulletins, surplus, needs, skills) | Geographic area |
_dm | Direct messages between specific nodes | Global (filtered by recipient node_id) |
_org_{slug} | Organization broadcasts | Global (filtered by org membership) |
_sync_{identifier} | Cross-device data sync for applications | Global (filtered by sync group membership) |
Special channels (prefixed with _) are not geographic. They route globally via Tier 3 and propagate via BLE when devices are in proximity. The _sync_ pattern enables VCN to function as a serverless cross-device synchronization layer for any application.
{country}_{region}._dm: fixed string. Direct messages include to and from fields in the payload for recipient filtering._org_{slug}: slug follows the same rules as org_scope (lowercase ASCII, digits, hyphens, max 64 chars)._sync_{identifier}: identifier is a node_id or application-defined group key. Max 64 characters.For the _dm channel, Tier 3 endpoints SHOULD support a to query parameter to filter messages server-side by recipient node_id. Without this, clients receive all global DMs and filter locally, which does not scale. The recommended endpoint: GET /vcn/packets?area_tag=_dm&to={node_id}.
Nodes exchange contact information via QR code. The QR data MUST be a JSON object:
{
"type": "vcn_contact",
"node_id": "Base64-URL Ed25519 public key",
"name": "Display name or null",
"enc_key": "Base64-URL X25519 public key or null",
"ts": 1775200000000
}
The enc_key field (added in v1.2) carries the node's X25519 encryption public key for end-to-end encrypted messaging. Contacts exchanged without enc_key fall back to plaintext messaging. The TOFU (Trust On First Use) mechanism backfills the key when the first encrypted message arrives from a contact.
Scanning adds the contact locally. The scanner SHOULD also emit a contact_exchange packet (including enc_key) so the scannee receives the scanner's identity and encryption key on next sync (two-way exchange).
BLE discovery MUST use a universal service identifier (vcn.mesh) rather than an area-scoped identifier. All VCN devices connect to all nearby VCN devices regardless of area tag. The Bloom filter handshake (§6) exchanges ALL stored packets across ALL channels. Channel filtering occurs at the application layer, not the transport layer.
Every VCN packet, regardless of originating app or content type, uses the following envelope. The payload field is app-specific; its schema is determined by packet_type (see §4).
{
"vcn_version": "1.0", // REQUIRED — protocol version
"source_app": "eluvorim", // REQUIRED — registered app ID
"source_node": "X9kLm2vQ...", // REQUIRED — Base64-URL Ed25519 public key
"packet_id": "uuid-v4", // REQUIRED — globally unique, UUID v4
"packet_type": "goods", // REQUIRED — registered type string
"area_tag": "ph_cebu", // REQUIRED — routing region identifier
"timestamp": 1741478400000, // REQUIRED — Unix milliseconds UTC
"ttl": 72, // REQUIRED — hours, decremented per hop
"location": { /* see §5 */ }, // OPTIONAL — geohash location schema
"payload": { /* type-specific */ }, // REQUIRED — app data
"deep_link": "veydrin://...", // OPTIONAL — in-app navigation URI
"signature": "Base64-URL..." // REQUIRED — Ed25519 signature
}
The signature is computed over the canonical form of the packet. Canonical form is defined as:
signature field excluded from the serialized input.canonical_input ::= json_compact_sorted( packet \ { signature } )
signature ::= base64url( ed25519_sign( private_key, canonical_input ) )
Every node maintains a seen_packets set of packet_id values. Before processing or forwarding any received packet, the node checks this set. If the packet_id is present, the packet is silently discarded. If not, the packet_id is added and processing proceeds. The set is pruned of entries older than max_ttl (720 hours) to prevent unbounded growth.
When a node forwards a packet, it decrements the ttl field by 1. The signature is not recomputed on forward — the original emitter's signature remains intact. The TTL decrement is the only permitted modification to a packet in transit. At ttl = 0 the packet is not forwarded further.
| Packet type | Default TTL (hours) | Max TTL (hours) |
|---|---|---|
| goods / services / skills / need / groupbuy | 72 | 720 |
| surplus | 720 (30 days) | 2160 (90 days) |
| harvest / hive / observation | 168 (7 days) | 720 |
| bulletin / update | 168 | 720 |
| message | 72 | 168 |
All TTL values are user-configurable within the stated max. Apps should expose TTL as a simple duration selector, not a raw number.
surplus payload includes an expires unix timestamp set by the originator. TTL governs how many hops a packet will travel before it stops propagating. expires governs whether the listing is still active at the application layer. Both must be checked: a packet that has not expired but has reached TTL 0 stops propagating; a packet that is still propagating but whose expires has passed must not be displayed as an active listing. Receiving apps discard surplus packets whose expires timestamp is in the past.
| packet_type | Handler app | Deep link scheme |
|---|---|---|
goods | Eluvorim | veydrin://eluvorim/listing/{id} |
services | Eluvorim | veydrin://eluvorim/listing/{id} |
skills | Eluvorim | veydrin://eluvorim/listing/{id} |
need | Eluvorim | veydrin://eluvorim/need/{id} |
groupbuy | Eluvorim | veydrin://eluvorim/groupbuy/{id} |
surplus | Eluvorim / Numaka / Apiara / Chinara | veydrin://{app}/surplus/{id} |
harvest | Chinara | veydrin://chinara/harvest/{id} |
hive | Apiara | veydrin://apiara/hive/{id} |
observation | Numaka | veydrin://numaka/observation/{id} |
bulletin | VCN (any app) | veydrin://vcn/bulletin/{id} |
update | VCN (any app) | veydrin://vcn/update/{id} |
message | VCN (any app) | veydrin://vcn/message/{id} |
contact_exchange | VCN (any app) | N/A (processed internally) |
Area tags are human-readable routing region identifiers. Format rules:
{country_code}_{region} — e.g., ph_cebu, us_richmond_va, hn_tegucigalpa.The type map lookup is the first delivery filter. The second is the area tag. Even if packet_type maps to an installed handler, the packet is only delivered if the receiving node's configured area_tag matches the packet's area_tag field. Matching is exact string equality in v1.0. No wildcards. No hierarchical prefix matching. See §6 for the forwarding and sync logic that governs how packets move between nodes before delivery.
When a Veydrin app fires a deep link and the target app is not installed, the firing app must catch the failed intent and present a non-intrusive prompt offering the target app's Codeberg release page or F-Droid listing. It must not error, crash, or surface a system-level "no app" dialog.
All string fields are UTF-8. Fields marked optional may be omitted entirely — receiving apps must tolerate their absence gracefully.
{ "title": string, "value": number|null, "notes": string|null,
"currency": string|null, "image_hash": string|null }
{ "title": string, "notes": string|null, "offer": string|null }
{ "title": string, "min_count": integer, "unit_value": number|null,
"deadline": unix_ms|null, "notes": string|null }
{ "species": string, "quantity": string, "unit": string|null,
"contact": string, "expires": unix_ms, "intent": "trade"|"sell"|"give"|"barter"|"ask",
"notes": string|null, "image_hash": string|null }
{ "crop": string, "quantity": number, "unit": string,
"value": number|null, "harvest_date": unix_ms, "notes": string|null,
"image_hash": string|null }
{ "product": string, "quantity": number, "unit": string,
"value": number|null, "notes": string|null, "ohds_ref": string|null }
{ "guild_type": string, "layer": string, "succession_state": string|null,
"notes": string|null, "image_hash": string|null, "opds_ref": string|null }
{ "title": string, "body": string,
"target_app": string|null, "version": string|null, "url": string|null }
Messages use the _dm channel (see §2.2). The to field enables server-side recipient filtering. Two payload variants exist:
Plaintext (legacy/fallback):
{ "to": string, // recipient node_id
"from": string, // sender node_id
"text": string }
Encrypted (v1.2):
{ "to": string, // recipient node_id
"from": string, // sender node_id
"enc": string, // Base64-URL: nonce(12) + AES-256-GCM ciphertext + mac(16)
"enc_key": string, // sender's X25519 public key (Base64-URL, no padding)
"v": 1 }
Encrypted messages use X25519 ECDH key agreement (derived from Ed25519 node keys via HKDF-SHA256 with info string vcn-x25519-encryption-v1) + AES-256-GCM. The sender's enc_key is included so the recipient can decrypt without prior contact exchange. See §10 for the full encryption protocol.
Senders MUST encrypt when the recipient's encryption key is known. Senders MUST fall back to plaintext when the recipient's encryption key is unavailable (pre-v1.2 contacts).
{ "target": string, // node_id of the intended recipient
"name": string|null,
"enc_key": string|null }
Emitted after scanning a contact QR to enable two-way discovery. The enc_key field (v1.2) carries the sender's X25519 encryption public key. Processed internally by the VCN layer, not displayed to users.
Any community packet type (bulletin, surplus, need, skills) MAY include org broadcast fields per §2.1:
{ ...type_payload,
"org_scope": string, // org slug
"org_name": string|null, // display name
"org_signature": string }
Location is a first-class field in the VCN packet format. Every app that attaches location to a packet uses this schema. Exact GPS coordinates never appear in any VCN packet under any circumstances — the geohash is the privacy floor.
A geohash converts GPS coordinates into a short text code. Longer codes describe smaller, more precise areas. The code for any location is a prefix of the code for any smaller area it contains — enabling natural geographic hierarchy without central configuration.
| Precision | Code length | Approx. area | Use in VCN |
|---|---|---|---|
| Province / large region | 3 chars | ~150 km radius | Rural / sparse network fallback |
| Metro / greater area | 4 chars | ~40 km radius | Default routing key |
| District / neighborhood | 5 chars | ~5 km radius | Dense urban opt-in |
| Street level | 6 chars | ~1 km | Not used in routing |
{
"location": {
"geohash": "w3gv", // routing key at chosen precision
"geohash_precision": 4, // number of chars used
"coords_available": false // exact coords never in packet unless explicit opt-in
}
}
Geohash cells have hard edges. A device near a cell boundary should store and forward packets for all 8 neighboring cells automatically, in addition to its own cell. This prevents listings from being invisible to users just across a geohash boundary. No user configuration required — this is automatic behavior in vcn_node.
All VCN-enabled apps that display location use OpenStreetMap tiles via flutter_map. Offline tile caching is required — users must be able to download their region's tiles for use without connectivity. The app-level community map is an emergent property of VCN packets with location fields: each app registers which packet types it renders as map pins. The shared OSM layer makes the mesh visible.
All six tiers use the same packet format, signing, verification, deduplication, and type-map routing logic. They are complementary, not alternatives. A device uses all available tiers simultaneously. Tiers are listed in priority order for the target population.
All VCN communication over any network transport (Tiers 2, 3, 5, and 6) must use TLS 1.3 minimum. Certificate validation is enforced. No fallback to plaintext under any circumstances. Tier 1 (physical proximity via Android Nearby Connections API) and Tier 4 (QR) do not use TLS — they are local channel transports that operate entirely off-network.
hive packets — it has no delivery handler, but it must still relay them to peers. Discarding at delivery is correct. Discarding at forwarding is a protocol violation that silently breaks the mesh.
// FORWARDING — happens first, regardless of local handler
forward_packet(p):
if p.ttl <= 0: drop
if p.packet_id in seen_packets: drop
seen_packets.add(p.packet_id)
deliver_packet(p) // deliver BEFORE decrement — handler sees original TTL
p.ttl -= 1
if p.ttl > 0:
queue_for_all_transports(p)
// DELIVERY — only for packets we can handle locally
deliver_packet(p):
handler = type_map[p.packet_type]
if handler == null: return // no handler — not an error, packet still forwarded
if p.area_tag != local_area_tag: return
if handler_app_not_installed: return
route_to(handler, p)
When two nodes connect via a peer-to-peer tier (physical proximity, dedicated node, or QR), they negotiate what to exchange using a Bloom filter handshake. This avoids retransmitting packets the other node already has. This applies to Tiers 1, 2, and 4 only — Tier 3 (Cloudflare Workers) uses incremental HTTP polling via ?since=unix_ms and does not use this handshake.
// Both sides exchange their seen_packets Bloom filters
A → B: SYNC_HELLO { area_tag, bloom_filter(seen_packet_ids), node_id }
B → A: SYNC_HELLO { area_tag, bloom_filter(seen_packet_ids), node_id }
// Each side sends only packets the other node probably lacks
A → B: SYNC_PACKETS [ packets not in B's bloom filter ]
B → A: SYNC_PACKETS [ packets not in A's bloom filter ]
// Session closes
A ↔ B: SYNC_DONE
Parameters: 10,000 capacity, 1% false positive rate. This yields a bit array of 95,851 bits (~12 KB) with 7 hash functions. The hash algorithm is double hashing using SHA-256: h(i, item) = (SHA256(item)[0..15] + i * SHA256(item)[16..31]) mod m for i = 0..6. The bit array is transmitted as Base64-URL-encoded bytes, little-endian bit order.
False positives cause a missed sync for that packet — it will arrive in the next session. No data loss, only slight delay. The filter is rebuilt from the live seen_packets set at the start of each session.
Sync messages are length-prefixed JSON frames over the established transport connection (BLE, WiFi Direct TCP, or dedicated node TCP). Each frame is:
[4 bytes: payload length as big-endian uint32][N bytes: UTF-8 JSON payload]
Message types:
SYNC_HELLO — {"type": "sync_hello", "vcn_version": "1.0", "area_tag": "...", "bloom": "Base64-URL...", "node_id": "..."}. If vcn_version major versions do not match, the receiving node closes the connection immediately.SYNC_PACKETS — {"type": "sync_packets", "packets": [...]}. Array of full VCN packet objects. Sent after both sides have exchanged SYNC_HELLO.SYNC_DONE — {"type": "sync_done"}. Signals completion. Both sides close the connection after sending and receiving SYNC_DONE.Two nodes in physical proximity exchange packets directly, device to device. No internet, no infrastructure, no configuration. Uses Android Nearby Connections API (Bluetooth and WiFi Direct transport) via the nearby_connections Flutter package. mDNS handles discovery on any shared local network (community WiFi, home router, café hotspot).
Packet size constraint: Core text packets must not exceed 1 KB. Images are never included in proximity sync packets (see §8).
Discovery: Nodes advertise their area_tag as the service ID. A node only initiates connection to peers advertising a matching area tag, preventing cross-community leakage at the transport layer.
Session lifecycle: Sessions are opportunistic and short-lived. A session opens when two compatible nodes are discovered, exchanges the outbound packet queue, then closes. No persistent connection is maintained.
Any device running a VCN-enabled app may opt in as a dedicated node — a persistent, always-on regional anchor. Dedicated nodes are the community's own infrastructure. No Veydrin server is involved at any step.
Discovery: Dedicated nodes advertise themselves using a Kademlia-based DHT conforming to BEP 5 (Mainline DHT). The DHT key is the SHA-1 hash of the node's 4-character geohash string. The stored value is a compact contact record: {node_id, ip, port, last_seen}. Mobile devices query the DHT for their geohash key (and all 8 adjacent geohash cells) to find regional dedicated nodes and sync directly. DHT entries have a 30-minute TTL and are re-announced every 15 minutes by active dedicated nodes. The DHT stores only contact addresses, not content. No persistent record of who was where.
Bootstrap: New nodes join the DHT by querying the Tier 3 endpoint at GET /dht-bootstrap, which returns a list of 8 known DHT nodes as [{ip, port}]. After initial bootstrap, nodes discover peers through standard Kademlia routing and no longer depend on Tier 3 for DHT operations.
Activation: Available in Settings when charging is detected. User toggle — off by default. Charging + WiFi required. Battery use when active is negligible (WorkManager, Doze-compliant, fires only during sync windows).
Cold start: A dedicated node in a region solves cold start for all new users — new device connects to WiFi, finds the dedicated node in the DHT, syncs, and immediately has the region's packet history. No physical proximity required.
Old device repurposing: Any Android device (including old phones no longer used as primary) can serve as a dedicated node permanently. Plug in, open app, enable Node Mode. The device becomes a regional anchor for its community. No advanced setup. UPnP handles port forwarding automatically on consumer routers.
Storage cap: User-configurable. Suggested default: 500 MB. Devices store packets for their geohash region and all 8 adjacent cells. Auto-trim by age then by geohash distance when cap approaches.
When a device has internet connectivity and no dedicated node is yet available in its region, it pushes packets to and pulls from a lightweight HTTPS endpoint backed by a Cloudflare Worker and Cloudflare KV storage. This is the mechanism that connects the first users in any region before the dedicated node mesh fills in. It is the only component in the VCN stack with an external infrastructure dependency.
Architecture: One Cloudflare Worker handles both reads and writes. Packets are stored in Cloudflare KV keyed by {area_tag}:{packet_id} with a TTL matching the packet's ttl field. KV entries expire automatically — no manual cleanup. Cloudflare's free tier (100k requests/day, 1 GB KV) is sufficient for bootstrap use across all regions.
Push: POST /packets with the full VCN packet JSON as the request body. The Worker verifies the packet's Ed25519 signature against the source_node public key before storing. Packets with invalid or missing signatures are rejected with HTTP 400. No accounts. No API keys. No sessions. The signature is the authentication.
Pull: GET /packets?area_tag={tag}&since={unix_ms} — returns all stored packets for the given area tag with timestamps after since. Response is a JSON array. Incremental — clients track the timestamp of their last successful pull.
Global coverage: One Worker deployment serves all regions — every packet carries an area_tag and pull requests filter by it. Devices in Cebu receive only Cebu packets. No cross-regional leakage.
Rate limiting: The Worker enforces per-source-node rate limits: maximum 60 packet pushes per hour per source_node. Excess requests receive HTTP 429. This is a spam floor, not an access control mechanism.
Role: Once a region has dedicated nodes, Tier 3 becomes pure redundancy. It does not replace Tier 2 — it precedes it in regional adoption and backs it up permanently. The Worker source code is published in the VCN protocols repository under AGPL-3.0 and may be self-hosted by any community that prefers to eliminate this dependency.
Two nodes exchange a batch of packets by encoding them as a QR code (or sequence of QR codes) and scanning with the device camera. No internet, no Bluetooth pairing, no network of any kind. Requires only that both devices be in the same physical location.
Encoding: Base64-URL-encoded, gzip-compressed JSON array of packet objects. A single QR at max density (v40, error correction L) holds ~2.9 KB. Threshold for multi-frame: 2 KB (implementation-defined reference default — adjust after device testing on mid-range Android/GrapheneOS).
Multi-frame: { "frame": 1, "total": 3, "batch_id": "uuid", "data": "..." } — frames assembled in any order, duplicates ignored.
Primary use cases: Transaction records, listing discovery at physical markets, identity exchange, sync bootstrap for new devices with no connectivity of any kind.
VCN-enabled apps may optionally route communication through Tor hidden services (.onion addresses). This tier provides metadata privacy — the network location of the user is not revealed to destination nodes or network observers. Tor transport is opt-in and user-initiated. Apps must not silently route traffic through Tor without explicit user consent.
When to implement: Applications serving populations with elevated surveillance risk — asylum seekers, domestic violence survivors, political dissidents, LGBTQ+ communities in hostile jurisdictions.
Badge requirement: Required for the enhanced VCN trust badge (see Community Spec §7).
Apps may implement pluggable transports (obfs4, Snowflake, or equivalent) that disguise VCN traffic as innocuous traffic, evading deep packet inspection and censorship infrastructure. This tier is for deployments where Tor traffic itself is blocked or surveilled. Must be explicitly enabled by the user — not default.
When required: Applications deployed for communities in censored or authoritarian environments, or any app intending to earn the full VCN trust badge (Community Spec §7).
Dedicated Node Mode is a first-class feature of the VCN protocol — not a hidden setting, not a developer tool. Any user with a spare Android device can strengthen their community's network by enabling it. The device becomes a persistent regional anchor that serves all VCN-enabled apps in the area.
When Node Mode is active, the app collapses to a minimal status screen showing: packets stored, last sync timestamp, peers seen in the last 24 hours, and storage used. No advanced configuration required. UPnP handles port forwarding automatically on consumer routers — no manual setup.
Auto-trim runs two passes when storage approaches the configured cap (default 500 MB):
The node's own locally-generated packets are never trimmed regardless of age or cap.
| State | RAM | CPU | Network |
|---|---|---|---|
| Idle (advertising only) | ~15–25 MB | Negligible | Near zero |
| Active sync window | ~30–50 MB | Low burst | Only during sync |
| Long-running dedicated node | ~30–50 MB | WorkManager bursts only | Periodic, not persistent |
Images are never included in sync packets propagated through any tier. This is a firm protocol-level constraint, not a recommendation. Packet size limits and low-bandwidth target environments make inline images incompatible with reliable sync.
When a listing or record includes an image, the emitting node stores the image locally and includes only the SHA-256 hex digest (image_hash field) in the packet payload. Image transfer is a separate, out-of-band, bilateral, on-demand operation using direct HTTP over whichever transport tier connects the two nodes:
GET /image/{image_hash}. The request is authenticated by Ed25519-signed headers: X-VCN-Node: {node_id}, X-VCN-Ts: {unix_ms_timestamp}, X-VCN-Sig: {base64url(sign(UTF-8("{image_hash}:{unix_ms_timestamp}")))}. The receiver MUST reject requests where the timestamp differs from the receiver's current time by more than 300 seconds.Content-Type: image/*.image_hash.Transport: Image requests route via the best available tier — Tier 2 (dedicated node relay) preferred for cross-WiFi requests, Tier 1 (direct device-to-device) for proximity requests. Tier 3 (Cloudflare) is not used for image transfer. If no transport path exists to the source node at the time of request, the request is deferred until a sync session with the source node (or a node that has cached the image) becomes available.
The source node may decline any image request with HTTP 403. Declining does not affect packet delivery or listing visibility. Images are optional — listings without images are fully valid.
The vcn_node shared Flutter/Dart package is the single implementation of this protocol. All Veydrin apps import it. No app reimplements any part of the protocol directly.
All protocol logic lives in vcn_node: identity generation, packet format, signing, type map, transport, sync. All app-specific UX stays in the consuming app: community tab layout, listing rendering, partner card display, deep link handling UI. This boundary must be respected — coupling app UX to protocol logic makes extraction impossible.
Development sequence: vcn_node is built and proven in Eluvorim (first implementation) before extraction as a standalone published package. The extraction boundary defined here must be implemented before Eluvorim scaffolding begins.
VcnConfig.init({
required String sourceApp, // registered source_app identifier (compile-time constant)
required String areaTag, // community area tag — set once, persisted in Hive CE
})
sourceApp is a compile-time constant defined per app (e.g., "eluvorim"). areaTag is set during onboarding and stored in the app's local Hive CE box. It is also writable later via VcnSync.setAreaTag(). Both values are automatically attached to every emitted packet envelope — VcnEmitter.emit() does not require them as parameters.
VcnEmitter.emit({
required String packetType,
required Map<String, dynamic> payload,
Map<String, dynamic>? location, // geohash schema — see §5
String? deepLink,
int ttl = 72,
})
Builds the envelope, attaches source_node and source_app from local identity, signs, adds to the outbound queue for all active transports, returns the packet_id.
VcnReceiver.register( String packetType, Future<void> Function(VcnPacket packet) handler, )
Registers a callback for a given packet_type. Multiple handlers may be registered for the same type. Called after area tag filtering and deduplication pass.
VcnSync.start() // Begin all available transport tiers
VcnSync.stop() // Graceful shutdown
VcnSync.setAreaTag(tag) // Update active area tag filter
VcnSync.enableNodeMode(cap) // Activate dedicated node (charging + WiFi required)
VcnSync.disableNodeMode() // Deactivate dedicated node
// Returns current transport tier availability
VcnSyncStatus VcnSync.status() → {
tier1_active: bool, // BLE/WiFi Direct advertising
tier2_active: bool, // DHT connected to at least one dedicated node
tier3_active: bool, // Cloudflare endpoint reachable
tier4_active: bool, // always true — QR available when app is open
tier5_active: bool, // Tor circuit established
tier6_active: bool, // pluggable transport active
node_mode: bool, // dedicated node mode running
last_sync: int, // unix ms of last successful sync on any tier
}
VCN provides standard E2E encryption for direct messages between nodes:
vcn-x25519-encryption-v1 and empty salt. This is deterministic — the same master seed always produces the same encryption key.nonce(12) + ciphertext + mac(16) and encoded as Base64-URL without padding.contact_exchange packets (see §2.2).enc_key from the message payload is automatically saved. This enables encryption without prior explicit contact exchange.enc_key differs from the stored key for that contact, the receiver SHOULD flag the message to the user as a potential key change. Decryption proceeds using the sender-provided key.source_node, the receiver SHOULD flag the message to the user as a potential downgrade. The message is still delivered but visually distinguished. This prevents silent stripping of encryption by a relay or attacker.text field. Receivers MUST accept both formats.seen_packets set is pruned of entries older than 720 hours. Packets replayed after pruning could be accepted as new. Mitigation: receivers SHOULD reject packets whose timestamp is older than the packet type's maximum TTL. For packet types with TTLs exceeding 720 hours (e.g., surplus at 2160 hours), the seen_packets retention for those entries SHOULD be extended to match.Added in VCN Protocol v1.3.
VCN has no central authority and no global moderation. Trust and safety are enforced locally by each device and collectively by each community. The following mechanisms form the trust layer.
Receivers MAY maintain a list of blocked source_node identifiers. Packets from blocked nodes MUST be dropped on receive before any other processing. Blocked nodes' packets MUST NOT be forwarded — the blocking device does not relay them to any tier. Blocks are local to the device and are not broadcast to the mesh. Blocks SHOULD auto-expire after 90 days of inactivity to prevent stale lists.
When a user blocks a node, the application SHOULD emit a flag packet to the local area channel:
{ "target": string } // source_node ID of the flagged node
Receivers tally flag packets per target from unique source nodes. A flag is only counted if the flagging node has contributed 3 or more non-flag packets to the local store (anti-Sybil gate). When the count reaches a community threshold (default: 5 unique flaggers), the target is permanently blocked with no expiry. This is local consensus; each device makes its own decision based on the flags it has received. There is no global ban. A flagged node retains access to other areas and to direct messaging.
Flag packets use the same area_tag as community posts and propagate via all tiers. They have a TTL of 720 hours (30 days).
Tier 3 relays MUST enforce per-source_node rate limits in addition to per-IP limits. Recommended limits:
Tier 1 (BLE) implementations SHOULD limit inbound connections to 10 per minute to prevent handshake flooding.
Tier 3 relays MAY require proof-of-participation for community post types. A node must have a valid VRS counter record (from any Veydrin application) before its community posts are accepted. Direct messages and contact exchange packets are exempt — they always route. This prevents drive-by spam from freshly generated node identities.
Packet payloads MUST NOT exceed 8,192 bytes (8 KB) when serialized as JSON. Receivers MUST reject packets exceeding this limit. Tier 3 relays enforce a 16 KB total request body limit which encompasses the full envelope.
During Tier 1 BLE sync handshake, receivers MUST reject bloom filters exceeding 16 KB (approximately 21,334 Base64-URL characters). This prevents memory exhaustion from malicious peers transmitting oversized filter data.
Once a receiver has successfully verified an org_signature for a given org_scope against a founder public key, that key is pinned locally. Subsequent packets claiming the same org_scope but signed by a different key MUST be rejected. This prevents org impersonation even if the org slug is known. Pinning is local and does not require a central registry.
This section defines what it means to be a conforming VCN implementation. Four conformance targets exist. An implementation MAY satisfy one or more targets.
An application that creates and signs VCN packets. MUST satisfy:
packet_id. Never reused.An application that accepts and processes VCN packets. MUST satisfy:
source_node before delivering any packet to application handlers. Discard silently on failure.seen_packets set. Discard packets with a previously seen packet_id.area_tag. Also deliver packets on subscribed special channels: _dm (filtered by recipient node_id), _org_{slug} (filtered by org membership), _sync_{id} (filtered by sync group). See §2.2 for channel patterns.packet_type MUST be forwarded (if the node forwards) but MUST NOT cause errors. Silently ignore for local delivery.A node that relays packets to other nodes. Every forwarder MUST also satisfy VCN Receiver rules. Additionally:
ttl by 1 on forward. Do not forward packets with ttl = 0.A persistent, always-on regional anchor. MUST satisfy all Forwarder rules plus:
An application that supports organization broadcasts. MUST satisfy:
org_scope, org_signature, and the signature MUST be computed per §2.1 (sign org_scope:title).org_signature against the stored founder key. Discard on failure. Ignore if not a member.vcn_org_key_version, slug, public_key, private_key_enc, nonce, salt, kdf, kdf_iterations.All VCN applications SHOULD support identity portability:
VCN Protocol Spec uses semantic versioning (MAJOR.MINOR). The vcn_version envelope field is the designated compatibility mechanism.
| Change type | Version bump | Compatibility |
|---|---|---|
| Add optional envelope field; add optional payload schema field; add new packet type; clarify existing behavior | MINOR |
Backward compatible. Old nodes ignore unknown fields. |
| Change required envelope field; change canonical serialization; change signing algorithm; restructure type map; change transport requirement | MAJOR |
Breaking. Nodes receiving a packet with an unsupported major version drop it silently and log locally. No exotic negotiation. |
| Correct typographic or formatting error without changing behavior | Patch note — no version bump | No implementation change required. |
vcn_version field is sufficient. Minor versions are backward compatible by definition — no negotiation needed.
The VCN Protocol Spec and the VCN Community Spec version independently. Protocol changes do not require community governance changes and vice versa.
| Term | Definition |
|---|---|
| Area tag | A community-chosen string identifier for a geographic region (e.g., ph_cebu, us_austin). Used for packet routing and filtering. Not a geohash — area tags are human-readable names. |
| Bloom filter | A probabilistic data structure that tests whether a packet ID has been seen. May produce false positives (causing a missed sync) but never false negatives. |
| Canonical serialization | The deterministic JSON encoding used for signing: keys sorted alphabetically, compact form, UTF-8, signature field excluded. |
| Dedicated node | A device running in always-on mode that stores and forwards packets for its region. Acts as a persistent regional anchor in the mesh. |
| Deep link | A URI (veydrin://...) embedded in a packet that directs the receiving app to a specific screen or action. |
| Delivery | Processing a packet locally — passing it to registered handlers in the receiving app. Distinct from forwarding. |
| DHT | Distributed Hash Table. A decentralized key-value store used for dedicated node discovery. VCN uses Mainline DHT (BEP 5, Kademlia-based). |
| Forwarding | Relaying a packet to other nodes without local processing. The TTL is decremented but the packet is otherwise unmodified. Nodes MUST forward all valid packets regardless of local interest. |
| Geohash | A string encoding of geographic coordinates into a grid cell. VCN uses 4-character geohashes (~40 km precision) as the privacy floor for location data. |
| Node | Any device running a VCN-enabled application. Every node has a unique Ed25519 identity and can emit, receive, and forward packets. |
| Node ID | The Base64-URL-encoded Ed25519 public key of a node. 44 characters, no padding. This is the node's permanent, self-certifying identity. |
| Packet | The atomic unit of data in VCN. A signed JSON envelope containing metadata (type, area tag, TTL, timestamp) and a type-specific payload. |
| source_app | A registered string identifier set at compile time that identifies which Veydrin application emitted a packet (e.g., apiara, eluvorim). |
| Transport tier | One of six layered communication channels: T1 Physical Proximity (BLE/WiFi Direct), T2 Dedicated Nodes (DHT), T3 HTTPS (Cloudflare Workers), T4 QR Physical Exchange, T5 Tor, T6 Pluggable Transports. |
| TTL | Time to live. An integer field on each packet representing remaining hops. Decremented by 1 at each forwarding node. Packet is not forwarded when TTL reaches 0. |
| vcn_node | The shared Flutter/Dart package that implements the entire VCN protocol. All Veydrin apps import it rather than reimplementing protocol logic. |
| Reference | Title | Relevance |
|---|---|---|
| RFC 8032 | Edwards-Curve Digital Signature Algorithm (EdDSA) | Ed25519 for node identity and packet signatures. |
| NIST SP 800-38D | Galois/Counter Mode (GCM) | AES-256-GCM for identity export encryption. |
| RFC 8259 | The JavaScript Object Notation (JSON) Data Interchange Format | Packet envelope and payload serialization. |
| RFC 9562 | Universally Unique Identifiers (UUIDs) | packet_id is UUID v4. |
| RFC 4648 §5 | Base64url Encoding | Node ID and signature encoding. |
| FIPS 180-4 | Secure Hash Standard (SHA-256) | Image content addressing. |
| RFC 8446 | Transport Layer Security (TLS) 1.3 | Required for Tier 3 HTTPS transport. |
| RFC 2119 | Key words for use in RFCs | Normative keyword definitions. |
| RFC 8174 | Ambiguity of Uppercase vs Lowercase in RFC 2119 | Uppercase keywords carry normative weight. |
| VODS v1.0 | Veydrin Open Data Standard | Data interoperability baseline for VCN member applications. |
| BEP 5 | DHT Protocol (Mainline DHT) | Kademlia-based distributed hash table for Tier 2 dedicated node discovery. |
| RFC 8018 | PKCS #5: Password-Based Cryptography (PBKDF2) | Key derivation for identity export passphrase. |
| Version | Date | Changes |
|---|---|---|
1.3.1 |
2026-04 | Encryption downgrade detection: receivers SHOULD flag plaintext messages from contacts with known encryption keys. Anti-Sybil gate for community flagging: flagging node must have 3+ non-flag packets before flags count toward permablock threshold. |
1.3 |
2026-04 | Added §10.1 Trust & Safety: local block lists with auto-expiry, community flagging with permablock threshold, per-node rate limiting (community/DM/other categories), proof-of-participation gate (VRS counter required for community posts), 8 KB packet payload limit, BLE connection rate limiting (10/min), bloom filter size validation (16 KB max), organization signature pinning. New flag packet type (30-day TTL). |
1.2 |
2026-04 | End-to-end encrypted messaging: X25519 ECDH (HKDF-derived from Ed25519) + AES-256-GCM for direct messages. TOFU key backfill from message payloads. Key rotation detection. Contact exchange now includes enc_key field. Encrypted message payload variant (enc, enc_key, v fields). Standalone org key export format (O5) — PBKDF2-SHA512 + AES-256-GCM, enables sharing org signing authority independently of node identity. Dedicated nodes now pull from 8 adjacent geohash cells (D3 regional storage). Incremental sync for node mode polling. |
1.1 |
2026-04 | Added §2.1 Organization Founder Identity (separate Ed25519 keypair for org ownership, independent from node identity). Added §2.2 Channel Patterns (geographic area tags, _dm for direct messages, _org_{slug} for org broadcasts, _sync_{id} for cross-device sync). BLE discovery MUST use universal vcn.mesh service ID. Org broadcast payload fields defined (org_scope, org_name, org_signature). |
1.0 |
2026-03 | Initial release — node identity (Ed25519), packet format with canonical signing, eleven packet types, geohash location privacy, six transport tiers, dedicated node mode, image transfer protocol, vcn_node package interface. |