<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jim</title>
    <description>The latest articles on DEV Community by Jim (@vesvaultjz).</description>
    <link>https://dev.to/vesvaultjz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024540%2F6d1cdbd9-c5f9-456b-8543-6e016469d1d9.png</url>
      <title>DEV Community: Jim</title>
      <link>https://dev.to/vesvaultjz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vesvaultjz"/>
    <language>en</language>
    <item>
      <title>Your browser tab just earned a real, publicly trusted TLS certificate</title>
      <dc:creator>Jim</dc:creator>
      <pubDate>Sun, 12 Jul 2026 23:51:30 +0000</pubDate>
      <link>https://dev.to/vesvaultjz/your-browser-tab-just-earned-a-real-publicly-trusted-tls-certificate-1n0h</link>
      <guid>https://dev.to/vesvaultjz/your-browser-tab-just-earned-a-real-publicly-trusted-tls-certificate-1n0h</guid>
      <description>&lt;p&gt;I wanted to create a demo that does something browsers were never meant to do: the browser tab generates a private key, obtains a real, publicly trusted TLS certificate for a real public hostname, and then serves live HTTPS to anyone on the internet — say, your phone. No account, no install, and the private key never leaves the tab.&lt;/p&gt;

&lt;p&gt;The working demo is at &lt;a href="https://snif.host/demo/" rel="noopener noreferrer"&gt;snif.host/demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is about how that works: running a TLS &lt;em&gt;server&lt;/em&gt; in WebAssembly over memory buffers, getting a CA to issue a certificate via &lt;code&gt;fetch()&lt;/code&gt;, and the pile of gotchas in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do this to a browser
&lt;/h2&gt;

&lt;p&gt;The demo exists because of &lt;a href="https://github.com/vesvault/snif" rel="noopener noreferrer"&gt;SNIF&lt;/a&gt; — SNI-based TLS Forwarder, an Apache-2.0 solution that gives a device behind NAT a browser-trusted HTTPS hostname without a public IP, a port forward, or handing its TLS private key to a tunnel provider. The device keeps its own key; a public relay forwards still-encrypted streams by their TLS SNI record. TLS terminates on the device, so the relay can't read or alter anything. This &lt;a href="https://datatracker.ietf.org/doc/draft-zubov-snif/" rel="noopener noreferrer"&gt;IETF Internet-Draft&lt;/a&gt; describes the SNIF protocol suite.&lt;/p&gt;

&lt;p&gt;That claim — &lt;em&gt;the relay can't read your traffic&lt;/em&gt; — is the kind of claim you shouldn't take from a README. Diagrams prove nothing. But a browser tab is itself a device behind NAT: it has no public IP, no listening sockets, no inbound reachability at all. Even worse — its outbound capabilities are limited to HTTPS and WebSockets, no raw TCP/UDP sockets. If a browser tab can become a SNIF connector, it would be running in one of the most locked-down network environments there are.&lt;/p&gt;

&lt;p&gt;So that became the spec: the tab must do everything a native SNIF connector does. Generate the key. Get the cert. Accept visitors' TLS handshakes. Serve pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shapes everything: browsers have no sockets
&lt;/h2&gt;

&lt;p&gt;A SNIF connector talks to the relay over raw TCP. Browser JavaScript — and browser wasm — has no raw TCP and no listening sockets. That's a sandbox boundary, not a wasm limitation; Emscripten's POSIX socket emulation just tunnels over WebSockets anyway, and the Direct Sockets API is restricted to Isolated Web Apps, unusable for a public page.&lt;/p&gt;

&lt;p&gt;So something has to translate browser transport into the relay's TCP. The answer is a websockify-style WebSocket↔TCP bridge sitting next to the relay: every byte of the connector's relay connections rides a binary WebSocket to the bridge, which splices it onto a plain TCP socket. Two fixed routes (&lt;code&gt;/snif/ctl&lt;/code&gt; → the relay's control port, &lt;code&gt;/snif/srv&lt;/code&gt; → its forward pool), fixed targets only — the bridge can't be coerced into connecting anywhere else, so there's no SSRF surface.&lt;/p&gt;

&lt;p&gt;Crucially, the relay daemon itself is not modified. The bridge is pure demo infrastructure, and — this matters for the trust model — it only ever sees opaque bytes. The end-to-end TLS is terminated inside the tab, not at the bridge. The &lt;code&gt;wss://&lt;/code&gt; layer is just transport framing.&lt;/p&gt;

&lt;p&gt;The bridge is ~300 lines of dependency-free PHP — a single non-blocking &lt;code&gt;stream_select()&lt;/code&gt; loop with its own RFC 6455 handshake and frame de-framing. Not because PHP is anyone's idea of a WebSocket runtime, but because the host it runs on is a PHP stack, and one small &lt;code&gt;php-cli&lt;/code&gt; daemon beats importing Node onto a relay host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plot twist: the browser has to be the TLS &lt;em&gt;server&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the architectural fact that shaped all the crypto work. In the SNIF protocol, on the control connection &lt;strong&gt;the relay is the TLS client and the connector is the TLS server&lt;/strong&gt;. That's not an accident — it's how the connector proves it holds the private key for its hostname: it completes a TLS handshake &lt;em&gt;presenting its certificate&lt;/em&gt;, and the relay matches the hostname the connector claims against the certificate's CN.&lt;/p&gt;

&lt;p&gt;And when a visitor connects, the relay blind-pipes the visitor's raw TLS bytes to the connector, which has to &lt;code&gt;SSL_accept&lt;/code&gt; them — again, be the server.&lt;/p&gt;

&lt;p&gt;So "run a SNIF connector in a tab" really means: &lt;strong&gt;run a TLS 1.3 server inside WebAssembly, with no sockets, over memory buffers fed by a WebSocket.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the wasm small: only the TLS engine goes in
&lt;/h2&gt;

&lt;p&gt;OpenSSL compiled to wasm is famously heavy and painful. The demo uses &lt;a href="https://www.wolfssl.com/" rel="noopener noreferrer"&gt;wolfSSL&lt;/a&gt; instead, and — more importantly — draws a hard line on what goes into wasm at all:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;th&gt;wasm cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SNIF protocol framing (text lines)&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key generation + CSR&lt;/td&gt;
&lt;td&gt;wolfCrypt wasm shim&lt;/td&gt;
&lt;td&gt;~155 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate issuance HTTP flow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetch()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket transport&lt;/td&gt;
&lt;td&gt;JS &lt;code&gt;WebSocket&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS server over memory buffers&lt;/td&gt;
&lt;td&gt;wolfSSL wasm shim&lt;/td&gt;
&lt;td&gt;~535 KB, lazy-loaded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The TLS module only loads when you click "Make it live" — the certificate-issuance page stays light.&lt;/p&gt;

&lt;p&gt;The TLS-server shim is the interesting part. wolfSSL lets you replace its transport with callbacks (&lt;code&gt;wolfSSL_CTX_SetIORecv&lt;/code&gt; / &lt;code&gt;wolfSSL_CTX_SetIOSend&lt;/code&gt;), so the wasm module runs a TLS server against two in-memory byte queues and never touches a socket API. JavaScript feeds inbound WebSocket bytes into one queue and drains outbound bytes from the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a real certificate with fetch()
&lt;/h2&gt;

&lt;p&gt;SNIF's certificate issuance runs through a CA proxy that fronts ACME (Let's Encrypt does the actual issuing). The native connector drives it with libcurl; the whole flow can run on HTTPS or on plain HTTP:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GET&lt;/code&gt; the init endpoint → the relay allocates you a hostname zone and returns it in an &lt;code&gt;X-SNIF-CN&lt;/code&gt; header. It's a wildcard, like &lt;code&gt;*.snif-xxxx-yyyy.snif.xyz&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Generate an EC key and a PKCS#10 CSR in wasm, with the full wildcard CN as the subject.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PUT&lt;/code&gt; the CSR to the cert API.&lt;/li&gt;
&lt;li&gt;Authorize. The native connector prints an authorization link for a human to open in a browser; the demo is already &lt;em&gt;in&lt;/em&gt; a browser, so it authorizes with a CAPTCHA instead — you solve a Cloudflare Turnstile widget and the token goes up in an &lt;code&gt;X-SNIF-Captcha&lt;/code&gt; header. This is the only human step, and it's what rate-limits a free public cert API against abuse (per-IP and global daily caps).&lt;/li&gt;
&lt;li&gt;Poll for the &lt;code&gt;.crt&lt;/code&gt; — you get &lt;code&gt;503&lt;/code&gt; while ACME is doing its thing, then &lt;code&gt;200&lt;/code&gt; with a real, browser-trusted certificate chain. Typically well under 30 seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The default zero-conf behavior of the SNIF connector is to use plain &lt;code&gt;http://&lt;/code&gt; for the CA proxy calls — from an HTTPS page that's a hard mixed-content block, not a warning you can click through. The connector protocol already had an API-base override, so the demo points it at the HTTPS init origin and the whole flow stays on one secure origin, with CORS opened up for the &lt;code&gt;PUT&lt;/code&gt; and the custom headers.&lt;/p&gt;

&lt;p&gt;The private key never goes anywhere in this flow. The CA sees a CSR (public key + signature); the certificate comes back for the key that was generated in your tab and lives in your tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas, in the order they drew blood
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A PKCS#10 CSR's version must be 0.&lt;/strong&gt; wolfSSL's &lt;code&gt;wc_InitCert&lt;/code&gt; defaults the version field to X.509v3 (that's &lt;code&gt;2&lt;/code&gt;), which is correct for certificates and fatally wrong for CSRs. OpenSSL — and therefore Let's Encrypt — rejects the signature. One line (&lt;code&gt;req.version = 0&lt;/code&gt;), found the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;wolfSSL needs &lt;code&gt;--disable-asm&lt;/code&gt; for wasm.&lt;/strong&gt; wolfCrypt's &lt;code&gt;cpuid.c&lt;/code&gt; contains x86 inline assembly that obviously won't compile to wasm. Fine — but not documented anywhere as a wasm recipe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do NOT &lt;code&gt;--disable-filesystem&lt;/code&gt; in Emscripten, even though you have no files.&lt;/strong&gt; wolfCrypt seeds its RNG from &lt;code&gt;/dev/urandom&lt;/code&gt;, which Emscripten emulates via &lt;code&gt;crypto.getRandomValues&lt;/code&gt;. Disable the virtual filesystem to save size and the RNG init fails with a cryptic &lt;code&gt;-199&lt;/code&gt;, taking all of &lt;code&gt;wolfSSL_Init&lt;/code&gt; down with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RSA-4096 keygen in wasm is brutally slow.&lt;/strong&gt; The native SNIF connector's lowest-common-denominator fallback is RSA-4096; in wasm that's a multi-second stall. The demo generates EC keys instead — and the CA proxy advertises its preferred curve in a response header (the public relay currently steers P-384), with the demo honoring the policy and falling back to P-256. The relay is key-type-agnostic; nothing in the protocol pins the algorithm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your hostname is derived from your key.&lt;/strong&gt; The allocated CN is a wildcard zone; the connector's actual hostname is a stable label under it, derived from a digest of the private key DER. It's reproducible by the connector, which holds the private key, but cannot be derived by anyone who has only the wildcard zone CN. The per-device label never appears in CT logs, only the wildcard does — deliberate hostname privacy. Amusing detail: in the SNIF connector code, the digest is MD5 (a non-security, label-only use). The connector in the browser does not need to use exactly the same derivation rules as the original SNIF code, as long as the hostname is stable per connector. But it was a matter of principle. So the demo carries a tiny pure-JS MD5 for exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving HTTPS from a tab
&lt;/h2&gt;

&lt;p&gt;With a cert and the TLS engine in place, the connector logic itself is small, transport-neutral JavaScript that mirrors the relay's actual source:&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;control connection&lt;/strong&gt; is pure TLS from byte zero — remember, the relay connects as a TLS &lt;em&gt;client&lt;/em&gt;, so the tab's wasm TLS server accepts the relay's handshake, presenting its shiny new certificate. Then it sends &lt;code&gt;SNIF LISTEN &amp;lt;hostname&amp;gt;&lt;/code&gt; as application data and waits.&lt;/p&gt;

&lt;p&gt;When a visitor hits &lt;code&gt;https://&amp;lt;your-hostname&amp;gt;/&lt;/code&gt;, the relay sends &lt;code&gt;SNIF CONNECT&lt;/code&gt; on the control channel. The tab opens a second WebSocket to the bridge, sends a plaintext &lt;code&gt;SNIF ACCEPT &amp;lt;connid&amp;gt;&lt;/code&gt; preamble, and then the relay blind-pipes the visitor's raw TLS bytes through. The tab's wasm TLS server accepts &lt;em&gt;that&lt;/em&gt; handshake too, and answers the HTTP request with a small live page.&lt;/p&gt;

&lt;p&gt;At no point does the relay — or the bridge — hold a key or see plaintext. What they see: which hostname (that's the SNI routing, it's the design), source IPs, timing, volume. What they can't do: read, modify, or impersonate. Impersonation would require getting a &lt;em&gt;new&lt;/em&gt; certificate mis-issued for the name, which lands in public Certificate Transparency logs — and SNIF ships &lt;a href="https://github.com/vesvault/snif/tree/master/snifdog" rel="noopener noreferrer"&gt;snifdog&lt;/a&gt;, a CT watchdog pinned to your key's SPKI, so that move is detected rather than trusted away. The demo doesn't ask you to believe the trust model; it lets you run both ends of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than just one page
&lt;/h2&gt;

&lt;p&gt;A single web page served by a browser tab is already a proof of concept. But I wanted the demo to do something a visitor would actually &lt;em&gt;use&lt;/em&gt;, so it grew into a small file-sharing server, served entirely by the tab. Drop files into the demo page and each one becomes a real &lt;code&gt;https://&lt;/code&gt; link under your hostname; anyone who opens your URL sees a live listing, and can send files back to your tab.&lt;/p&gt;

&lt;p&gt;Under the hood that's an actual — if minimal — web server, in the same transport-neutral JavaScript, on top of the wasm TLS engine. Requests come off the decrypted stream and get routed: &lt;code&gt;GET /&lt;/code&gt; renders the listing page, &lt;code&gt;GET /d/&amp;lt;id&amp;gt;&lt;/code&gt; serves a file (inline, or as an attachment with &lt;code&gt;?dl=1&lt;/code&gt;), &lt;code&gt;PUT /u/&amp;lt;name&amp;gt;&lt;/code&gt; accepts a visitor upload — raw body, no multipart — capped at 25 MB so a stranger can't balloon the tab's memory. Responses are hand-assembled HTTP/1.0 byte arrays: status line, &lt;code&gt;Content-Length&lt;/code&gt;, &lt;code&gt;Content-Disposition&lt;/code&gt;, &lt;code&gt;Connection: close&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The part that made me grin: &lt;strong&gt;long-polling works&lt;/strong&gt;. The listing page holds a &lt;code&gt;GET /events&lt;/code&gt; request open, and the connector defers the response — a promise that resolves when the file list changes, or times out politely with a &lt;code&gt;204&lt;/code&gt;. So when you drop a file into your tab, everyone currently viewing your page sees it appear, live, no reload. A browser tab, holding a hanging request open for its own visitor, over a TLS session it terminates itself.&lt;/p&gt;

&lt;p&gt;Two mundane bits of realism turned out to matter: multi-megabyte downloads are paced against the WebSocket's send queue instead of being buffered whole (backpressure, or the tab's memory eats the file twice), and a visitor who disconnects mid-request tears down any deferred response so the tab doesn't accumulate dead waiters.&lt;/p&gt;

&lt;p&gt;And the trust property carries all the way through: the file bytes exist in your tab's memory and in the visitor's browser, and nowhere in between. The relay forwards ciphertext. It's a file transfer through infrastructure that can't see the files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;p&gt;The demo optimizes for "no install, no account, nothing to clean up" — and that shows in a few corners worth being honest about.&lt;/p&gt;

&lt;p&gt;The private key and certificate live in the browser's &lt;code&gt;localStorage&lt;/code&gt;. That's plaintext, same-origin-readable storage — acceptable for a throwaway &lt;code&gt;*.snif.xyz&lt;/code&gt; demo identity that gates nothing, and it's what lets a reload come back as the same hostname instead of burning another cert issuance. A real SNIF connector keeps its key on the device it protects; don't put a TLS key that matters in localStorage because a demo did.&lt;/p&gt;

&lt;p&gt;The shared files are the opposite: memory only, never persisted anywhere. Reload the tab and your identity survives but the files are gone. And your server has the uptime of a browser tab — close it, or let the laptop sleep, and your hostname goes dark until the tab is back.&lt;/p&gt;

&lt;p&gt;Open the demo in two tabs and it gets funnier: both read the same stored key, so both claim the same hostname, and the relay — by design — announces each incoming visitor connection to every connector listening on that name. Whichever tab answers first serves the request. Since each tab's file list is its own in-memory state, a visitor can get different content from one request to the next. Entertaining to watch; not a mode you'd ship.&lt;/p&gt;

&lt;p&gt;None of these are protocol limits — a native connector has real key storage, real persistence, and one process per hostname. They're the price of doing it in a tab, which is the whole stunt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The demo: &lt;a href="https://snif.host/demo/" rel="noopener noreferrer"&gt;https://snif.host/demo/&lt;/a&gt; — solve one CAPTCHA, watch a real cert get issued to your tab, click "Make it live", open the URL on your phone. (Issuance is capacity-limited; if it asks you to come back later, that's the rate limiting working.)&lt;/li&gt;
&lt;li&gt;The demo's source (the connector JS, the wolfSSL wasm shims, the PHP bridge, the mock-relay tests — GPLv3): &lt;a href="https://github.com/vesvault/snif-demo" rel="noopener noreferrer"&gt;https://github.com/vesvault/snif-demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;On an actual device (outbound-only, no published ports): &lt;code&gt;docker run -d -v snif-etc:/etc/snif ghcr.io/vesvault/snif&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The repo (connector, relay, CA proxy, watchdog — all Apache-2.0, all self-hostable): &lt;a href="https://github.com/vesvault/snif" rel="noopener noreferrer"&gt;https://github.com/vesvault/snif&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The protocol: &lt;a href="https://datatracker.ietf.org/doc/draft-zubov-snif/" rel="noopener noreferrer"&gt;draft-zubov-snif&lt;/a&gt; · The trust model, precisely: &lt;a href="https://github.com/vesvault/snif/blob/master/security-model.md" rel="noopener noreferrer"&gt;security-model.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The questions I'm most hoping for are the skeptical ones — what exactly does the relay see, and how would I catch it misbehaving? That boundary is the whole point. Ask away.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>security</category>
      <category>tls</category>
      <category>showdev</category>
    </item>
    <item>
      <title>End-to-end encrypted collaborative notes in ~300 lines of JavaScript — no app server</title>
      <dc:creator>Jim</dc:creator>
      <pubDate>Sat, 11 Jul 2026 19:32:40 +0000</pubDate>
      <link>https://dev.to/vesvaultjz/end-to-end-encrypted-collaborative-notes-in-300-lines-of-javascript-no-app-server-2mo</link>
      <guid>https://dev.to/vesvaultjz/end-to-end-encrypted-collaborative-notes-in-300-lines-of-javascript-no-app-server-2mo</guid>
      <description>&lt;p&gt;I wanted to create a small demo of &lt;code&gt;libVES&lt;/code&gt; capabilities — E2EE per-record sharing, user roles, live events, history log. The result is &lt;strong&gt;&lt;a href="https://github.com/vesvault/vespost" rel="noopener noreferrer"&gt;VESpost&lt;/a&gt;&lt;/strong&gt;, end-to-end encrypted collaborative sticky notes. About 300 lines of plain JS, no dependencies other than &lt;code&gt;libVES&lt;/code&gt;, plus a CSS file and a basic HTML page. Runs entirely on VES APIs, no app server, just static hosting.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://vesvault.github.io/vespost/" rel="noopener noreferrer"&gt;try it live on GitHub Pages&lt;/a&gt;. It's Apache-2.0, and it's deliberately small enough to read in one sitting, because it's really a template for building this kind of app on &lt;a href="https://ves.host/docs/libVES-subtle" rel="noopener noreferrer"&gt;libVES.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post walks through the load-bearing lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why E2EE apps are usually painful
&lt;/h2&gt;

&lt;p&gt;If you've tried to build one, you know the four walls you hit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sharing.&lt;/strong&gt; Encrypting your own data is easy. Getting ciphertext readable by &lt;em&gt;another person&lt;/em&gt; means key exchange, and most designs push that pain onto users or compromise the E2E nature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access control.&lt;/strong&gt; Once data is encrypted client-side, "who can read, who can write, who can re-share" becomes a key-management problem in addition to a database column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key loss.&lt;/strong&gt; The classic E2EE trade-off: if the user loses their key, the data is gone. This single issue kills most consumer E2EE ideas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit.&lt;/strong&gt; "Who changed this record, when, from where?" — it gets more complicated when the server can't read the data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;VESpost gets all four from the library. The notes UI is a thin wrapper around a more general primitive: a &lt;strong&gt;shared, versioned, end-to-end encrypted record store&lt;/strong&gt; hosted by the VES repository at &lt;code&gt;ves.host&lt;/code&gt;. All plaintext exists only inside the browser tab.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Browser (index.html + vespost.js)                     ves.host
  +-------------------------------+                  +--------------+
  | libVES.subtle('demo')         |  e2e-encrypted   | VES Repo     |
  |  . encrypt/decrypt in-browser |&amp;lt;---ciphertext---&amp;gt;|  . items     |
  |  . key exchange for sharing   |                  |  . sharing   |
  |  . VESpost UI (this repo)     |&amp;lt;---live events---|  . history   |
  +-------------------------------+                  +--------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bootstrap: one domain, one unlock
&lt;/h2&gt;

&lt;p&gt;Everything starts in &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;libVES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// pick a VES domain&lt;/span&gt;

&lt;span class="c1"&gt;// on the Launch button:&lt;/span&gt;
&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VESpost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;demo_lock&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;VES domain&lt;/strong&gt; is your app's namespace. &lt;code&gt;demo&lt;/code&gt; is a public shared sandbox — perfect for a demo, wrong for anything real; you'd register an &lt;code&gt;x-yourapp&lt;/code&gt; experimental domain and change that one string.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unlock()&lt;/code&gt; opens the standard VES popup where the user signs into (or creates) a &lt;strong&gt;vault&lt;/strong&gt; — their set of keys. This is also where the key-loss problem is handled: VES offers multiple redundancy options, which is the thing that makes at-rest E2EE practical. (Details of the recovery design are on &lt;a href="https://vesvault.com" rel="noopener noreferrer"&gt;vesvault.com&lt;/a&gt;; it's the interesting part of VES but out of scope for this walkthrough.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscribe to everything, then render
&lt;/h2&gt;

&lt;p&gt;The whole sync engine, from the &lt;code&gt;VESpost&lt;/code&gt; constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onitemadd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onitemremove&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onitemcreate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onitemdelete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onitemchange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onauthexpire&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// vault about to auto-lock&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* order the cards */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;start(false)&lt;/code&gt; replays just enough of the stored event history to reconstruct the current state — each surviving note arrives as an event, building &lt;code&gt;this.items&lt;/code&gt; — and then keeps streaming &lt;strong&gt;live&lt;/strong&gt; events. So the same code path renders the initial screen and applies remote changes as other users make them. No separate "fetch the list" code needed.&lt;/p&gt;

&lt;p&gt;(&lt;code&gt;start(0)&lt;/code&gt; would replay the &lt;em&gt;entire&lt;/em&gt; history from the beginning — we'll use that for the audit log below.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Read and write: one call each
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;item&lt;/strong&gt; is one end-to-end encrypted, versioned record. A note is one item, and the entire persistence layer is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// encrypt val in-browser, store a new version&lt;/span&gt;
&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="c1"&gt;// fetch + decrypt the latest version&lt;/span&gt;
&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// may this vault overwrite? (drives read-only UI)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saving in VESpost is a 2-second-debounced &lt;code&gt;put()&lt;/code&gt; of the whole textarea. That's honest about what it is: if users with write permission are editing the same note at the same time, &lt;strong&gt;last-write-wins&lt;/strong&gt;. The UI flags a &lt;code&gt;changed&lt;/code&gt; state if a remote version lands while you're editing, but it doesn't merge — real co-editing would need a CRDT or field-level merge &lt;em&gt;on top of&lt;/em&gt; items.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing: the part that's normally a research project
&lt;/h2&gt;

&lt;p&gt;This is the exchange that justifies the whole approach. To share a note with someone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@acme.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;   &lt;span class="c1"&gt;// e2e key exchange happens here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. VES performs the end-to-end key exchange to that user's vault — no key server to operate, no fingerprint-verification ceremony to walk users through. If the recipient doesn't have a vault yet, a temp key is created, and once the recipient sets up their VES, the E2EE key exchange is completed on your side — by your VESpost session if one is open, or by the VES service worker in the background.&lt;/p&gt;

&lt;p&gt;Roles ride on the same call. VESpost's share form grants &lt;code&gt;admin&lt;/code&gt; (the right to edit and re-share) by pushing a second URI in a special domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// the recipient's email or vault uri&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If input.value was always an email, we could just do&lt;/span&gt;
    &lt;span class="c1"&gt;// add.push('//.admin/' + input.value)&lt;/span&gt;
    &lt;span class="c1"&gt;// instead of the next two lines. The ref properly handles a vault uri.&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;libVES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUri&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;externalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalId&lt;/span&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the rest of the ACL surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;// -&amp;gt; vaults, each with .owner/.admin/.current flags&lt;/span&gt;
&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// revoke a share&lt;/span&gt;
&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refuse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;// decline a note someone shared with you&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;share()&lt;/code&gt; is what renders the access list on each card; the flags drive the owner/admin badges. The item's owner always retains control; removing yourself from someone else's note just drops your own access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change log
&lt;/h2&gt;

&lt;p&gt;Every version of every item carries author metadata in the VES repository's log. VESpost's "History Log" (in the item's hamburger menu) replays an item's full history — &lt;code&gt;item.start(0)&lt;/code&gt; this time — and reads it out per event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;                    &lt;span class="c1"&gt;// when&lt;/span&gt;
&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;          &lt;span class="c1"&gt;// which version&lt;/span&gt;
&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;short&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// who&lt;/span&gt;
&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessid&lt;/span&gt;         &lt;span class="c1"&gt;// which session&lt;/span&gt;
&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;         &lt;span class="c1"&gt;// from what IP&lt;/span&gt;
&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;      &lt;span class="c1"&gt;// on what device&lt;/span&gt;
&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;            &lt;span class="c1"&gt;// the value at that version (still e2e)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what this is and isn't: the &lt;em&gt;values&lt;/em&gt; are end-to-end encrypted — &lt;code&gt;get()&lt;/code&gt; on a historical version decrypts in your browser like any other read — while the &lt;em&gt;attribution&lt;/em&gt; (who/when/where) is an &lt;strong&gt;authoritative log kept by the VES repository&lt;/strong&gt;, visible only to the item's participants. You're trusting the repository to record it honestly, the same way you trust a Git host's audit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately doesn't do
&lt;/h2&gt;

&lt;p&gt;A reference app is only useful if it's honest about its edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Last-write-wins&lt;/strong&gt;, as covered — no merging of concurrent edits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online-first.&lt;/strong&gt; No offline cache; the app expects to reach &lt;code&gt;ves.host&lt;/code&gt;. A production app should at least surface a clear "disconnected" state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;demo&lt;/code&gt; domain is a shared namespace.&lt;/strong&gt; While all items are E2EE, don't use it for production apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a template, not a product.&lt;/strong&gt; No theming, no hardening pass, no feature-completeness — three files you're meant to fork.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ship your own
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vesvault/vespost
&lt;span class="nb"&gt;cd &lt;/span&gt;vespost
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8080   &lt;span class="c"&gt;# any static server works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To turn it into &lt;em&gt;your&lt;/em&gt; app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;swap &lt;code&gt;'demo'&lt;/code&gt; for your own &lt;code&gt;x-*&lt;/code&gt; domain (which can later be swapped for your final name without &lt;code&gt;x-&lt;/code&gt; to have a real production look on VES);&lt;/li&gt;
&lt;li&gt;replace the sticky-note UI with whatever your records actually are;&lt;/li&gt;
&lt;li&gt;host the static files anywhere — GitHub Pages, S3, nginx.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration surface you saw above — &lt;code&gt;unlock&lt;/code&gt;, &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;put&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;/&lt;code&gt;share&lt;/code&gt;/&lt;code&gt;remove&lt;/code&gt;, and the event handlers — is the whole thing. The full method reference is on &lt;a href="https://ves.host/docs/libVES-subtle" rel="noopener noreferrer"&gt;ves.host&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you build something on it, I'd genuinely like to hear how the API holds up — issues and PRs welcome on &lt;a href="https://github.com/vesvault/vespost" rel="noopener noreferrer"&gt;vesvault/vespost&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>encryption</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
