Veydrin Community Network

VCN Protocol Spec v1.4

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.

VCN-PROTOCOL v1.4 AGPL-3.0 STABLE
§ 1

Overview & Design Philosophy

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 zero-infrastructure principle. A network that works at a market with no signal is a network that serves people who have been excluded from every other network. The primary sync mechanism requires no internet, no data plan, no server, and no configuration. Two phones with a VCN-enabled app installed are sufficient to form a network.

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.

Open participation. Participation in the VCN mesh requires no organizational membership. Any application implementing this protocol is a valid node. Installing a VCN-enabled app makes a device part of the network. No Veydrin approval, no account, no registration.
Conventions. The key words "MUST", "MUST NOT", "SHOULD", and "MAY" in this document follow RFC 2119 and RFC 8174.

Relationship to VODS

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.

§ 2

Node Identity

Key generation

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.

Node ID format

The node ID is the Base64-URL-encoded Ed25519 public key, 43 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"
Self-certifying identity. No central authority can issue, vouch for, or revoke a node identity. Any device can generate a valid node ID. Trust is established through packet signing verification, not through a registry.

Packet signing

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.

Identity export and migration

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
}

Key derivation

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.

Registered source_app identifiers

The source_app envelope field identifies the originating application. The following identifiers are registered. Third-party VCN implementations must use a distinct identifier not listed here:

source_app valueApplicationStatus
eluvorimEluvorim, Community LedgerReserved
melisaeMelisae, Beekeeping ManagementActive
chinaraChinara, Soilless CultivationReserved
numakaNumaka, PermacultureReserved
arcaloxArcalox, Personal Resilience LoggingReserved
omnavarOmnavar, Grant Discovery & ManagementActive
vcnVCN infrastructure packets (bulletin, update, message, contact_exchange)Active
apiaraApiara, superseded by melisaeDeprecated

Status values. Active, in use by a released application; receivers MUST accept it. Reserved, allocated to a planned application and MUST NOT be used by any other implementation. Deprecated, no longer emitted by any current release; receivers MUST continue to accept it so that records and packets from installs predating the rename remain valid, and MUST NOT treat it as equivalent to its successor for deduplication.

Registry maintenance. This table is normative. Renaming an application is a change to this registry and MUST be applied in the same pass to: this table, the type map in §4, the deep link scheme for the affected packet types, and every VRS relay's allowed-application set. An identifier is never reused for a different application.

source_app is set at compile time as a package constant in each consuming app's vcn_node configuration. It is not user-configurable.

§ 2.1

Organization Founder Identity

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.

Why separate keys

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.

Org founder key lifecycle

Org-scoped broadcast payload fields

FieldTypeReq?Description
org_scopestringreqOrg slug. Lowercase ASCII, digits, hyphens only. Max 64 characters.
org_namestringoptHuman-readable org name for display. Max 128 characters.
org_signaturestringreqBase64-URL Ed25519 signature (86 characters, no padding) of org_scope + : + broadcast title, signed with the org founder private key. If the title is empty, sign org_scope alone.

Verification behavior

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.

Org QR code format

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 signature test vector

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.

Security considerations for org identity

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.

§ 2.2

Channel Patterns

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:

PatternPurposeScope
{area_tag}Local community packets (bulletins, surplus, needs, skills)Geographic area
_dmDirect messages between specific nodesGlobal (filtered by recipient node_id)
_org_{slug}Organization broadcastsGlobal (filtered by org membership)
_sync_{identifier}Cross-device data sync for applicationsGlobal (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.

Channel format rules

Tier 3 server-side filtering

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}.

Contact exchange QR format

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 transport and channels

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.

§ 3

Packet Format

Envelope schema

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 (emission)
  "clock_asserted": true,                // REQUIRED v1.4, does the emitter vouch for timestamp?
  "transit":       { ... }               // REQUIRED v1.4, MUTABLE, excluded from signature
  "ttl":           72,                  // REQUIRED, hours. Immutable hop budget; transit.hops_remaining decrements, not this.
  "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
}

Canonical serialization for signing

The signature is computed over the canonical form of the packet. Canonical form is defined as:

canonical_input  ::=  json_compact_sorted( packet \ { signature, transit } )
signature        ::=  base64url( ed25519_sign( private_key, canonical_input ) )

A JSON Schema for the packet envelope is published alongside this document at vcn-packet-v1.4.schema.json.

Canonical packet signing test vector

This vector is reproducible from a fixed Ed25519 seed, so any implementation can regenerate every value below and confirm it matches. A conforming implementation MUST produce the same canonical input for this packet and MUST verify the signature against the node's public key.

Private seed (hex, 32 bytes): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f

node_id (Base64-URL Ed25519 public key): A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg

enc_key (Base64-URL X25519 public key, HKDF-SHA256 derived from the seed with info vcn-x25519-encryption-v1): Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc

Canonical input (sorted keys at all levels, compact, UTF-8, 479 bytes; this exact byte string is what gets signed):

{"area_tag":"gcpv","clock_asserted":true,"enc_key":"Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc","location":{"coords_available":false,"geohash":"gcpv","geohash_precision":4},"packet_id":"3f2504e0-4f89-41d3-9a0c-0305e82c3301","packet_type":"bulletin","payload":{"body":"Community meeting Saturday at the market.","title":"Meeting Saturday"},"source_app":"melisae","source_node":"A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg","timestamp":1700000000000,"ttl":168,"vcn_version":"1.0"}

Signature (Base64-URL, no padding, 86 characters):

Bup0U49fNpALvR2GafYgaqxPW0dURRC4LXs8_yQZ3KPZEZFSNitlT8gfF_Gc6CtcsKtZxtBCvwpb94MIDwOzBw

Full signed packet (the canonical input with the signature field added):

{"area_tag":"gcpv","clock_asserted":true,"enc_key":"Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc","location":{"coords_available":false,"geohash":"gcpv","geohash_precision":4},"packet_id":"3f2504e0-4f89-41d3-9a0c-0305e82c3301","packet_type":"bulletin","payload":{"body":"Community meeting Saturday at the market.","title":"Meeting Saturday"},"source_app":"melisae","source_node":"A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg","timestamp":1700000000000,"ttl":168,"vcn_version":"1.0","signature":"Bup0U49fNpALvR2GafYgaqxPW0dURRC4LXs8_yQZ3KPZEZFSNitlT8gfF_Gc6CtcsKtZxtBCvwpb94MIDwOzBw"}

To verify: Base64-URL-decode node_id to the 32-byte public key, Base64-URL-decode the signature to 64 bytes, and check the Ed25519 signature over the UTF-8 bytes of the canonical input. Any change to any signed field alters the canonical input and MUST cause verification to fail. This vector was generated by the reference implementation and independently verified by a second Ed25519 library.

Deduplication

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.

The transit block, mutable state

Added in v1.4. Every field outside the transit block is immutable from emission through delivery and is covered by the emitter's signature. All state that legitimately changes as a packet crosses the mesh lives inside transit, which is excluded from the canonical form (§3) and therefore from the signature.

"ttl":     168,                  // IMMUTABLE, signed. The ORIGINAL hop budget. Never modified in transit.
"transit": {                     // MUTABLE, unsigned. Forwarders modify this and nothing else.
  "hops_remaining": 165,         // REQUIRED. Initialised to ttl by the emitter, decremented on each forward.
  "elapsed_ms":     10800000,    // REQUIRED. Milliseconds since emission, accumulated in transit.
  "prev_node":      "X9kLm..."   // OPTIONAL. Node ID of the immediately preceding forwarder.
}
Why this exists, the defect it fixes. Before v1.4, ttl was a signed field that forwarders were required to decrement (§11 W2), while the canonical form excluded only signature (§3). Those two rules cannot both hold: once a forwarder decrements ttl, the receiving node recomputes a canonical form that no longer matches what the emitter signed, so verification fails, and §11 R1 requires that such packets be silently discarded. The practical consequence was that a packet could not survive more than one hop, and the failure produced no error anywhere. Freezing ttl and moving the mutable counter into an unsigned transit block resolves the contradiction and restores multi-hop forwarding.

Compatibility

This is an additive change. ttl keeps its name, position, type and range, and remains inside the signature; only its meaning narrows from "current hop budget" to "original hop budget." Packets emitted before v1.4 carry no transit block, so the revised canonical form produces byte-identical input for them and existing signatures continue to verify. A v1.4 node therefore interoperates with an older node in both directions, and two v1.4 nodes forward correctly across an arbitrary number of hops.

Forwarding rules

Transit integrity remains advisory. The transit block is unsigned, so a malicious forwarder can still inflate or deflate its own copy of the hop counter and the elapsed time. What v1.4 changes is that this can no longer invalidate the emitter's signature or silently destroy the packet: tampering degrades routing for that branch instead of removing the packet from the network. Treat transit as a routing hint, never as a security property. Hop-by-hop authentication of the transit block is deferred to a future revision.

Packet age and expiry

Added in v1.4. A packet's age determines whether it has expired. Age MUST NOT be derived from wall-clock arithmetic alone, because the emitting node's clock may be wrong by an arbitrary amount and many target devices have no reliable time source at all.

The emitter declares whether its clock is trustworthy, in the immutable clock_asserted field:

Every forwarder adds the time it held the packet to transit.elapsed_ms before passing it on. A node that cannot measure elapsed time reliably MUST leave the value unchanged rather than guess.

packet_age(p):
  if p.clock_asserted and receiver_clock_is_trusted:
    wall_age = now - p.timestamp
    return max(wall_age, p.transit.elapsed_ms)   // elapsed_ms is a floor
  else:
    return p.transit.elapsed_ms

expired(p):  packet_age(p) > p.ttl * 3_600_000   // ttl is expressed in hours

Taking the maximum of the two measures means a forged-early timestamp cannot make a packet look fresher than the mesh has demonstrably observed it to be, while a node with no clock at all still expires packets correctly. Receivers MUST still reject packets whose timestamp is more than 5 minutes in the future when clock_asserted is true.

Age is not order. elapsed_ms establishes how old a packet is, which is sufficient for expiry and storage trimming. It does not establish the order in which packets were created, and receivers MUST NOT sort by timestamp and present the result as causal sequence, with unsynchronised clocks a reply can carry an earlier timestamp than the message it answers. Causal ordering is a separate concern and is not solved in v1.4.

Default TTL values

Packet typeDefault TTL (hours)Max TTL (hours)
goods / services / skills / need / groupbuy72720
surplus720 (30 days)2160 (90 days)
harvest / hive / observation168 (7 days)720
bulletin / update168720
message72168

All TTL values are user-configurable within the stated max. Apps should expose TTL as a simple duration selector, not a raw number.

surplus, TTL vs expires field. The 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 whose transit.hops_remaining has reached 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.
§ 4

Packet Type Map & Payload Schemas

Type map, registered types v1.0

packet_typeHandler appDeep link scheme
goodsEluvorimveydrin://eluvorim/listing/{id}
servicesEluvorimveydrin://eluvorim/listing/{id}
skillsEluvorimveydrin://eluvorim/listing/{id}
needEluvorimveydrin://eluvorim/need/{id}
groupbuyEluvorimveydrin://eluvorim/groupbuy/{id}
surplusEluvorim / Numaka / Melisae / Chinaraveydrin://{app}/surplus/{id}
harvestChinaraveydrin://chinara/harvest/{id}
hiveMelisaeveydrin://melisae/hive/{id}
observationNumakaveydrin://numaka/observation/{id}
bulletinVCN (any app)veydrin://vcn/bulletin/{id}
updateVCN (any app)veydrin://vcn/update/{id}
messageVCN (any app)veydrin://vcn/message/{id}
contact_exchangeVCN (any app)N/A (processed internally)

Area tag format

Area tags are human-readable routing region identifiers. Format rules:

Area tag classes

An area tag belongs to exactly one of three classes. The class is determined by the tag's own form, so any node can classify a tag it has never seen without consulting a registry.

ClassFormHow a node obtains it
Derivedg{geohash}, the literal g followed by a 4-character geohash, e.g. gw3gvComputed on-device. No user input.
NamedAny other conforming string, e.g. ph_cebuJoined from a list of named areas already seen in traffic, or entered directly.
ReservedBegins with _, e.g. _dm, _org_{slug}Assigned by the protocol. See §2.2.

Derived tags, normative

Every conforming implementation MUST be able to compute a derived area tag, and MUST use one as the initial area on first launch. The derivation is fixed so that two applications in the same place produce byte-identical tags without coordination:

derived_area_tag(lat, lon)  ::=  "g" + geohash(lat, lon, precision = 4)

The geohash uses the standard Base32 alphabet defined in §5, lowercased, truncated to exactly 4 characters (~40 km). Implementations MUST NOT vary the precision, prefix, or alphabet. Where no location is available, the node has no derived tag and participates only in named areas it has been given.

Why this exists. Before this rule, the area tag was free text with an unenforced convention, so two neighbours could independently choose us_richmond_va and richmond and never see each other, and two different applications naming the same place had no reason to agree at all. Packets routed correctly and delivered nowhere, with no error surfaced. A derived tag makes co-location sufficient for interoperability and removes the most common cause of an inexplicably empty community view.

Named tags

Named tags remain fully supported and are how a community adopts an identity of its own, how an area larger or smaller than a geohash cell is expressed, and how people who are not co-located share a channel. A node MAY subscribe to any number of named areas alongside its derived one.

Implementations SHOULD offer named areas by presenting those already observed in local traffic, and SHOULD offer to adopt a peer's area during contact exchange, so that joining an existing community requires recognition rather than recall. Implementations MUST also provide a path to enter a named tag directly, without it having been seen first: this is the only way to reach a community that has no local presence yet, and the only way to convene one that is not tied to a place. That path SHOULD NOT be the primary affordance on the area screen, and it MUST NOT be the default first-launch experience.

A conforming implementation MUST always display the tag currently in use, in full, wherever the area is shown. A node's area determines what it receives and, for unencrypted community traffic, who can read what it posts; a user who cannot see which area they are in cannot reason about either.

A named tag is obscure, not private. An unguessable tag limits who happens to encounter a channel; it does not protect its contents. Community packets are signed and not encrypted (§10), forwarding is mandatory regardless of subscription (§6), and Tier 1 advertises the area tag as its service identifier, so relays carry the plaintext of channels they are not in, anyone who learns a tag can read its retained history, and the tag itself is visible to radio-range observers. Do not present an unguessable named tag to users as a private channel. See §10 for what is and is not protected.

Area tag filtering

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.

Deep link, handler not installed

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.

Payload schemas, normative v1.0

All string fields are UTF-8. Fields marked optional may be omitted entirely, receiving apps must tolerate their absence gracefully.

goods / services / skills

{ "title": string, "value": number|null, "notes": string|null,
  "currency": string|null, "image_hash": string|null }

need

{ "title": string, "notes": string|null, "offer": string|null }

groupbuy

{ "title": string, "min_count": integer, "unit_value": number|null,
  "deadline": unix_ms|null, "notes": string|null }

surplus (shared across apps)

{ "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 }

harvest (Chinara)

{ "crop": string, "quantity": number, "unit": string,
  "value": number|null, "harvest_date": unix_ms, "notes": string|null,
  "image_hash": string|null }

hive (Melisae)

{ "product": string, "quantity": number, "unit": string,
  "value": number|null, "notes": string|null, "ohds_ref": string|null }

observation (Numaka)

{ "guild_type": string, "layer": string, "succession_state": string|null,
  "notes": string|null, "image_hash": string|null }

bulletin / update

{ "title": string, "body": string,
  "target_app": string|null, "version": string|null, "url": string|null }

Payload schemas, v1.1 additions

message (VCN, direct messaging)

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).

contact_exchange (VCN, two-way contact add)

{ "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.

Org-scoped broadcast payload (any community type)

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 }
§ 5

Location Schema & Privacy Model

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.

What a geohash is

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.

PrecisionCode lengthApprox. areaUse in VCN
Province / large region3 chars~150 km radiusRural / sparse network fallback
Metro / greater area4 chars~40 km radiusDefault routing key
District / neighborhood5 chars~5 km radiusDense urban opt-in
Street level6 chars~1 kmNot used in routing

Location packet schema

{
  "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
  }
}

Privacy rules, normative

Edge cell handling

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.

OSM rendering

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.

§ 6

Sync Transport Tiers

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.

TLS baseline

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.

Forward vs deliver

Forward ≠ Deliver. A node must forward packets it cannot deliver locally. A phone running only Eluvorim will receive Melisae 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.transit.hops_remaining <= 0: drop
  if p.packet_id in seen_packets: drop
  seen_packets.add(p.packet_id)
  deliver_packet(p)         // deliver BEFORE decrement, handler sees the hop count as received
  p.transit.hops_remaining -= 1
  p.transit.elapsed_ms += time_held
  if p.transit.hops_remaining > 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)

Sync negotiation, P2P Bloom filter handshake (Tiers 1, 2, 4)

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

Bloom filter specification

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 wire format

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:

Physical proximity first. For the communities VCN is built to serve, markets, neighborhoods, community gatherings across the global south, people are regularly physically near each other. Tier 1 is not a fallback. It is the primary sync event.
Tier 1, Physical Proximity (BLE + WiFi Direct + mDNS) PRIMARY

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.

Tier 2, Dedicated Community Nodes (DHT) REGIONAL BACKBONE

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.

Tier 3, HTTPS Endpoint (Cloudflare Workers + KV) BOOTSTRAP / FALLBACK

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.

Tier 4, QR Physical Exchange ZERO INFRASTRUCTURE

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.

Tier 5, Tor Hidden Service OPTIONAL, PRIVACY

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).

Tier 6, Pluggable Transports (obfs4 / Snowflake) HIGH-THREAT TIER

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).

§ 7

Dedicated Node Mode

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.

What a dedicated node does

Activation requirements

Node Mode UX

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.

Old device repurposing. A phone that would otherwise collect dust becomes community infrastructure. Plug it in, open the app, enable Node Mode. That is the entire setup. The community builds its own network without knowing that is what they are doing.

Storage management

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.

Resource usage

StateRAMCPUNetwork
Idle (advertising only)~15-25 MBNegligibleNear zero
Active sync window~30-50 MBLow burstOnly during sync
Long-running dedicated node~30-50 MBWorkManager bursts onlyPeriodic, not persistent
§ 8

Image Transfer

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:

  1. Receiving node sends an image request to the source node: 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.
  2. Source node verifies the request signature, retrieves the image from local storage, and transmits it as the response body with Content-Type: image/*.
  3. Receiving node verifies that SHA-256(received bytes) == image_hash.
  4. On match: image is stored and displayed. On mismatch: image is discarded silently.

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.

§ 9

vcn_node Package Interface

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.

Extraction boundary

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 (initialization, call before start)

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

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

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

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
}
§ 10

Security Properties & Known Limitations

Properties

End-to-end encrypted messaging (v1.2)

VCN provides standard E2E encryption for direct messages between nodes:

Encrypted direct message test vector

Reproducible from two fixed Ed25519 seeds. A conforming implementation MUST derive the same X25519 keys, agree the same shared secret, and decrypt the payload to the plaintext. The nonce is fixed here so the vector is reproducible; a real sender MUST use a random nonce.

Sender seed (hex, 32 bytes): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f

Sender X25519 public key (Base64-URL): Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc

Recipient seed (hex, 32 bytes): 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f

Recipient X25519 public key (Base64-URL): ma9ZsxQoN-tlYRyKL5ykIg1KPl3M_skAGixg7avWmWg

Shared secret (X25519 ECDH, hex, 32 bytes): 304f4bd498cd17eb2ed9fa7b11c0caeb06f8d8a8333489571f45c8886bcf5040

Plaintext: Meet at the north gate at dusk.

Nonce (hex, 12 bytes, fixed for this vector only): 000102030405060708090a0b

Encrypted payload (Base64-URL, nonce + ciphertext + mac): AAECAwQFBgcICQoLqvsUlB5juwtxehuWoPlAa4nvV900lIRib7TF8vIUCcEZwQHR4Go-RiQWi5Eqv3Q

To verify: derive each X25519 key from its seed with HKDF-SHA256 (info vcn-x25519-encryption-v1, empty salt); the X25519 agreement of one side's private key with the other side's public key yields the shared secret; AES-256-GCM with that secret and the leading 12-byte nonce decrypts the remainder to the plaintext. This vector was produced by the reference implementation and independently verified by a second X25519 and AES-GCM library.

Known limitations

Sensitive data. VCN packets are designed for community economic and ecological data, listings, harvests, observations, announcements. They are not designed to carry personally identifying information, private messages, or sensitive individual records. Applications handling sensitive data (Refuge-class apps, health records) must encrypt payloads at the application layer and should implement Tiers 5 and 6.
§ 10.1

Trust & Safety

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.

Local block list

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.

Community flagging

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).

Per-node rate limiting

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.

Proof-of-participation

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 size limit

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.

Bloom filter validation

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.

Organization signature pinning

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.

§ 11

Conformance

This section defines what it means to be a conforming VCN implementation. Four conformance targets exist. An implementation MAY satisfy one or more targets.

VCN Emitter

An application that creates and signs VCN packets. MUST satisfy:

VCN Receiver

An application that accepts and processes VCN packets. MUST satisfy:

VCN Forwarder

A node that relays packets to other nodes. Every forwarder MUST also satisfy VCN Receiver rules. Additionally:

VCN Dedicated Node

A persistent, always-on regional anchor. MUST satisfy all Forwarder rules plus:

VCN Org Broadcaster (v1.1)

An application that supports organization broadcasts. MUST satisfy:

VCN Identity (v1.1)

All VCN applications SHOULD support identity portability:

Implementation Status

This section is non-normative and records the status of known implementations, following the convention of RFC 7942. It may be removed before any final publication.

Reference implementation: Melisae. An open-source (AGPL-3.0) Flutter application running on the web and Android is the reference implementation of this specification. It implements node identity (§2), packet signing and the canonical form (§3), the v1.4 transit block with multi-hop forwarding, v1.4 packet-age expiry with clock_asserted, encrypted direct messaging over X25519 and AES-256-GCM (§10), community flagging with org founder-key pinning, and the multi-tier sync transports (§6).

Test vectors. The canonical packet signing vector (§3) and the encrypted direct message vector (§10) are reproducible from fixed seeds and have been independently verified by a second, unrelated Ed25519, X25519, and AES-GCM library. This confirms the canonical form, signature, key agreement, and encryption are interoperable at the algorithm level.

Interoperability status, stated plainly. One reference implementation exists today. A recognised standard requires at least two independent, interoperating implementations built from separate code bases. This document and its test vectors are written so a second implementation can be built from the specification alone. That second implementation, and a documented interoperability test between the two, is the remaining work before this specification can claim interoperability.

§ 12

Versioning

VCN Protocol Spec uses semantic versioning (MAJOR.MINOR). The vcn_version envelope field is the designated compatibility mechanism.

Change typeVersion bumpCompatibility
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.
Version negotiation, closed. A node receiving a packet with an unsupported MAJOR version drops it silently and logs locally. No handshake, no rejection message, no exotic negotiation. The 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.

§ 13

Glossary

TermDefinition
Area tagA 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 filterA 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 serializationThe deterministic JSON encoding used for signing: keys sorted alphabetically, compact form, UTF-8, with the signature field and the transit block excluded.
Dedicated nodeA device running in always-on mode that stores and forwards packets for its region. Acts as a persistent regional anchor in the mesh.
Deep linkA URI (veydrin://...) embedded in a packet that directs the receiving app to a specific screen or action.
DeliveryProcessing a packet locally, passing it to registered handlers in the receiving app. Distinct from forwarding.
DHTDistributed Hash Table. A decentralized key-value store used for dedicated node discovery. VCN uses Mainline DHT (BEP 5, Kademlia-based).
ForwardingRelaying a packet to other nodes without local processing. Only the transit block is modified; the packet is otherwise byte-identical. Nodes MUST forward all valid packets regardless of local interest.
GeohashA string encoding of geographic coordinates into a grid cell. VCN uses 4-character geohashes (~40 km precision) as the privacy floor for location data.
NodeAny device running a VCN-enabled application. Every node has a unique Ed25519 identity and can emit, receive, and forward packets.
Node IDThe Base64-URL-encoded Ed25519 public key of a node. 43 characters, no padding. This is the node's permanent, self-certifying identity.
PacketThe atomic unit of data in VCN. A signed JSON envelope containing metadata (type, area tag, TTL, timestamp) and a type-specific payload.
source_appA registered string identifier set at compile time that identifies which Veydrin application emitted a packet (e.g. melisae, eluvorim).
Transport tierOne 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.
TTLTime 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_nodeThe shared Flutter/Dart package that implements the entire VCN protocol. All Veydrin apps import it rather than reimplementing protocol logic.
§ 14

Normative References

ReferenceTitleRelevance
RFC 8032Edwards-Curve Digital Signature Algorithm (EdDSA)Ed25519 for node identity and packet signatures.
NIST SP 800-38DGalois/Counter Mode (GCM)AES-256-GCM for identity export encryption.
RFC 8259The JavaScript Object Notation (JSON) Data Interchange FormatPacket envelope and payload serialization.
RFC 9562Universally Unique Identifiers (UUIDs)packet_id is UUID v4.
RFC 4648 §5Base64url EncodingNode ID and signature encoding.
FIPS 180-4Secure Hash Standard (SHA-256)Image content addressing.
RFC 8446Transport Layer Security (TLS) 1.3Required for Tier 3 HTTPS transport.
RFC 2119Key words for use in RFCsNormative keyword definitions.
RFC 8174Ambiguity of Uppercase vs Lowercase in RFC 2119Uppercase keywords carry normative weight.
VODS v1.0Veydrin Open Data StandardData interoperability baseline for VCN member applications.
BEP 5DHT Protocol (Mainline DHT)Kademlia-based distributed hash table for Tier 2 dedicated node discovery.
RFC 8018PKCS #5: Password-Based Cryptography (PBKDF2)Key derivation for identity export passphrase.
§ 15

Changelog

VersionDateChanges
1.4 2026-08 Immutable/mutable split. New unsigned transit block carrying hops_remaining, elapsed_ms and optional prev_node; every field outside it is immutable and signed. ttl is frozen as the original hop budget and MUST NOT be modified in transit. Canonical form now excludes transit as well as signature. Fixes a defect that prevented any packet from surviving more than one hop: decrementing the signed ttl invalidated the emitter signature, and §11 R1 requires silently discarding packets that fail verification. Packet age and expiry. New immutable clock_asserted field; expiry derives from accumulated elapsed_ms when the emitter cannot vouch for its clock, and from the greater of wall age and elapsed time when it can, so nodes with no reliable time source expire packets correctly. Additive and backward compatible: pre-1.4 packets carry no transit block, so their canonical form and existing signatures are unchanged.
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.