[{"data":1,"prerenderedAt":238},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:en":3,"api:\u002Fapi\u002Fprofile:":8,"api:\u002Fapi\u002Fcategories:en":20,"api:\u002Fapi\u002Fprojects\u002Fpaste-vault:en":164,"api:\u002Fapi\u002Fnotes:en":217},{"email":4,"lang":5,"name":6,"role":7},"hello@ewsirdev.example","en","Ewsirdev","Web developer: websites, landing pages and Go services",{"name":6,"handle":6,"contacts":9,"note":19},{"email":10,"github":13,"telegram":16},{"url":11,"label":4,"placeholder":12},"mailto:hello@ewsirdev.example",true,{"url":14,"label":15},"https:\u002F\u002Fgithub.com\u002Fewsir247","github.com\u002Fewsir247",{"url":17,"label":18},"https:\u002F\u002Ft.me\u002FEwsirdev","@Ewsirdev","email is a placeholder on a reserved domain: mail is not delivered. Telegram is the real channel. This file is the single source of contacts for the site, the contact block, JSON-LD sameAs, security.txt and llms.txt.",[21,50,78,100,136],{"slug":22,"title":23,"blurb":24,"variant":25,"palette":26,"layout":27,"count":28,"preview":29,"stack":45},"landings","Landing pages","One page, one job. Hand-written markup where possible, plus one WebGL experiment.","drop","cold","shots",6,[30,35,40],{"slug":31,"title":32,"shot":33,"hue":34},"landing-studio","Halftone: lighting studio","\u002Fshots\u002Flanding-studio-desktop-sm.webp",212,{"slug":36,"title":37,"shot":38,"hue":39},"landing-saas","Kill Switch: subscription tracker","\u002Fshots\u002Flanding-saas-desktop-sm.webp",182,{"slug":41,"title":42,"shot":43,"hue":44},"landing-event","Small Scale: conference","\u002Fshots\u002Flanding-event-desktop-sm.webp",124,[46,47,48,49],"CSS","HTML","GitHub Actions","JS",{"slug":51,"title":52,"blurb":53,"variant":54,"palette":55,"layout":56,"count":57,"preview":58,"stack":74},"sites","Multi-page sites","Sites with their own structure, routing and forms: from a client project to a set of niche pages.","lens","mono","frames",3,[59,64,69],{"slug":60,"title":61,"shot":62,"hue":63},"legal-multipage-site","Multi-page legal services website","\u002Fshots\u002Flegal-multipage-site-desktop-sm.webp",250,{"slug":65,"title":66,"shot":67,"hue":68},"niche-landings","Catalog of 10 niche demo landing pages","\u002Fshots\u002Fniche-landings-desktop-sm.webp",32,{"slug":70,"title":71,"shot":72,"hue":73},"docs-engine","Sverlo: a documentation generator in Node.js","\u002Fshots\u002Fdocs-engine-desktop-sm.webp",25,[48,75,76,77],"Vue 3","Bootstrap 5","Cloudflare Pages",{"slug":79,"title":80,"blurb":81,"variant":82,"palette":83,"layout":84,"count":85,"preview":86,"stack":97},"apps","Web apps","Interface plus server: auth, data, e-mail. Apps you sign in to.","merge","warm","phone",2,[87,92],{"slug":88,"title":89,"shot":90,"hue":91},"bad-club-booking-app","Bad Club: seat booking for a gaming coworking space","\u002Fshots\u002Fbad-club-booking-app-desktop-sm.webp",150,{"slug":93,"title":94,"shot":95,"hue":96},"typing-lab","typing-lab: a touch-typing trainer","\u002Fshots\u002Ftyping-lab-desktop-sm.webp",45,[75,77,98,99],"Express","GSAP",{"slug":101,"title":102,"blurb":103,"variant":104,"palette":105,"layout":106,"count":107,"langs":108,"preview":117,"stack":133},"backend","Backends","Services with an API, a database and tests: mostly Go, with some in Python and TypeScript. A language filter sits inside the section.","torus","acid","list",14,[109,112,115],{"lang":110,"count":111},"Go",12,{"lang":113,"count":114},"Python",1,{"lang":116,"count":114},"TypeScript",[118,123,128],{"slug":119,"title":120,"shot":121,"hue":122},"kanban-tasks","Amber: kanban board","\u002Fshots\u002Fkanban-tasks-desktop-sm.webp",344,{"slug":124,"title":125,"shot":126,"hue":127},"link-shortener","Short: link shortener","\u002Fshots\u002Flink-shortener-desktop-sm.webp",111,{"slug":129,"title":130,"shot":131,"hue":132},"booking-slots","Appointment booking","\u002Fshots\u002Fbooking-slots-desktop-sm.webp",65,[134,110,135,48],"Docker","SQLite",{"slug":137,"title":138,"blurb":139,"variant":140,"palette":55,"layout":141,"count":57,"langs":142,"preview":145,"stack":161},"cli","CLI tools","Terminal programs: one binary, clear flags, an exit code you can check in a script.","film","terminal",[143,144],{"lang":110,"count":85},{"lang":113,"count":114},[146,151,156],{"slug":147,"title":148,"shot":149,"hue":150},"gitpulse","GitPulse: git repository analytics","\u002Fshots\u002Fgitpulse-desktop-sm.webp",7,{"slug":152,"title":153,"shot":154,"hue":155},"logslice","logslice: log analysis with no dependencies","\u002Fshots\u002Flogslice-desktop-sm.webp",160,{"slug":157,"title":158,"shot":159,"hue":160},"envguard","envguard: a linter and a safe for .env files","\u002Fshots\u002Fenvguard-desktop-sm.webp",35,[162,134,48,163],"CLI","stdlib only",{"slug":165,"title":166,"type":101,"category":101,"lang":116,"live_url":167,"stack":168,"description":175,"highlights":176,"run":181,"hue":182,"og":183,"shots":184,"case":192,"case_lang":5,"prev":214},"paste-vault","Konvert: notes encrypted in the browser","https:\u002F\u002Fewsirdev-paste.ewsirdev.workers.dev",[116,169,170,171,172,173,174,48],"Cloudflare Workers","D1","Web Crypto","Vite","vitest","fast-check","A one-time note service on TypeScript, Cloudflare Workers and D1: the text is encrypted in the browser (AES-256-GCM), the key lives only in the URL fragment and never reaches the server. A note has a lifetime, an optional password and burns after the first read. A side project.",[177,178,179,180],"Encryption in the browser: AES-256-GCM, the key only in the URL fragment","Burn after reading is atomic: exactly one success out of 40 parallel reads","52 tests in the workerd runtime, including property tests and schema fuzzing","A live version on Cloudflare Workers + D1","npm install && npm run dev",215,"\u002Fog\u002Fen\u002Fpaste-vault.jpg",{"desktop":185,"desktop_sm":186,"mobile":187,"dw":188,"dh":189,"mw":190,"mh":191},"\u002Fshots\u002Fpaste-vault-desktop.webp","\u002Fshots\u002Fpaste-vault-desktop-sm.webp","\u002Fshots\u002Fpaste-vault-mobile.webp",1280,800,390,844,{"about":193,"features":196,"run":204,"notes":209},[194,195],"Konvert (paste-vault) is a one-time note service in TypeScript: the backend runs on Cloudflare Workers with a D1 (SQLite) database, the frontend is vanilla TypeScript and Vite with no frameworks. A side project.","The text is encrypted in the browser (AES-256-GCM); the key is generated there too and lives only in the link fragment after #, so the server stores a single ciphertext. D1 was chosen over KV for burn-after-reading: the read and the delete are one transaction, so a note is handed out exactly once.",[197,198,199,200,201,202,203],"Client-side encryption: a random 256-bit key in the URL fragment, an optional password through PBKDF2 (600k iterations) and HKDF; neither the key nor the password leaves the browser","A note lifetime from 10 minutes to 7 days, burn after the first read, deletion with a secret token","API: metadata without spending the note, an atomic read (DELETE ... RETURNING in one batch), cron cleanup of expired notes","A custom syntax highlighting tokenizer: tokens always join back into the source text","A strict CSP with no inline code, HSTS, nosniff, an Origin check, a per-IP rate limit, notes are served with noindex and no-store","52 automated tests in the workerd runtime: client crypto, fast-check property tests, request schema fuzzing, no key or text in the DB, TTL, delete rights, burn under 40 parallel reads","A browser scenario over CDP: two independent profiles, burn, password, XSS, TTL, axe without violations",[205,206,207,208],"git clone \u003Crepository on request> paste-vault","cd paste-vault","npm install","npm run dev   # a local Worker with D1 emulation",[210,211,212,213],"If you lose the link with the key, the note is lost: there is no recovery. The size, creation time and view count are visible to the server, the text length is not masked.","The \"exactly one success\" test ran on 40 parallel reads in a local D1 emulation and on 12 parallel requests to the live Worker; it is not a proof for any load.","The IP limit is based on CF-Connecting-IP: networks sharing an address hit it sooner, and it does not protect against distributed floods. Expired notes stop being served at once but are physically deleted by a cron job every 30 minutes.","There is no external audit, it is a learning project.",{"slug":215,"title":216},"catalog-api","catalog-api: a catalog API with search",[218,223,228,233],{"slug":219,"lang":5,"title":220,"date":221,"summary":222,"project":129,"minutes":114},"double-booking","Double booking: a transaction plus a partial UNIQUE index","2026-10-20","How booking-slots protects against two clients booking the same time at once, and how that protection is tested.",{"slug":224,"lang":5,"title":225,"date":221,"summary":226,"project":227,"minutes":85},"lease-and-fencing","Lease and fencing: how a queue neither loses a job nor lets a zombie finish it twice","A worker can crash, stall, or wake up after its job was taken by someone else. How the lease and the lease token in job-queue deal with that on SQLite.","job-queue",{"slug":229,"lang":5,"title":230,"date":221,"summary":231,"project":232,"minutes":85},"p95-nearest-rank","p95 in an uptime monitor: the nearest-rank method and what it does not hide","How uptime-monitor computes p50\u002Fp95 latency and uptime, why the nearest-rank method was chosen, and where those numbers can mislead.","uptime-monitor",{"slug":234,"lang":5,"title":235,"date":221,"summary":236,"project":237,"minutes":85},"websocket-from-scratch","WebSocket on the standard library: what really happens under new WebSocket()","Why realtime-rooms has its own RFC 6455 layer, which protocol violations it checks, and why a library is still the better choice for production.","realtime-rooms",1792819120146]