# Tessel Stijn Punt — portfolio - [Portfolio website](https://tesselpunt.com/) I’m Tessel. A 22-year-old software engineer and founder who cares about how a product feels—and how it works. I take things from first sketch to real people using them. ## About Making an interface feel right. Figuring out the architecture behind it. Shipping, listening, and doing it again. I’m the founder of Bridge Labs, where I build consumer apps and internal tools. I work across React, TypeScript, APIs, and production operations, collaborating with a designer and a senior Unity developer. Previously, I contributed to frontend work at Leads.io and developed apps with LetPeopleWork. I was also invited to a Tools for Humanity / World build week in South Korea, where I placed third in the pitch competition. ## Profile and CV - [CV (PDF)](https://tesselpunt.com/cv/tessel-punt.pdf) - [GitHub](https://github.com/bridge-applications) - [LinkedIn](https://www.linkedin.com/in/tessel-punt-085321276/) - [Email](mailto:tspunt1@gmail.com) ## Projects - [Pebbler® for Telegram](https://tesselpunt.com/projects/pebbler-telegram.md): A little world built for play. A low-agency social game with character customisation, career progression, and rewards. - [Pebbler® on World](https://tesselpunt.com/projects/pebbler-world.md): A consumer-facing micro-task app connecting people with surveys, A/B comparisons, and play-and-earn quests. - [Pebbler® Admin Dashboard](https://tesselpunt.com/projects/pebbler-admin.md): An operations console with AI workflows for localising game headers and producing quest artwork. - [Bridge](https://tesselpunt.com/projects/bridge.md): Browser-based file sharing, with direct WebRTC transfers and cloud upload flows. - [Asset One](https://tesselpunt.com/projects/asset-one.md): A mobile portfolio interface for exploring balances, asset prices and transaction history. - [Figma Agent Context](https://tesselpunt.com/projects/figma-context.md): A Figma plugin that gives coding agents structured design context and visual references. - [Shared libraries](https://tesselpunt.com/projects/libraries.md): Authentication and React state-management packages reused across Bridge Labs applications. - [Voiced Gnome](https://tesselpunt.com/projects/voiced-gnome.md): Eighteen animated characters with individual voices and backstories. Browse the cast, hear their introductions and ask a question. - [tg-auth](https://tesselpunt.com/projects/tg-auth.md): Typed Telegram authentication, server verification, and encrypted application sessions shared between browser clients and backend services. - [world-auth](https://tesselpunt.com/projects/world-auth.md): World wallet sign-in, session-bound challenge verification, and encrypted credentials reused across Pebbler, Bridge, and World mini games. - [provider-kit](https://tesselpunt.com/projects/provider-kit.md): How I built a reusable React state toolkit with schema-inferred types, runtime validation, and one provider for independent state slices. --- # Pebbler® for Telegram - [Case study](https://tesselpunt.com/projects/pebbler-telegram) A little world built for play. A low-agency social game with character customisation, career progression, and rewards. From the first spin to the smallest feedback state. Role: Product engineering, frontend & backend Period: 2026 — present Technologies: React, TypeScript, Game UI, Cloudflare - [Live application](https://t.me/pebbler_bot) I build and operate Pebbler, a low-agency social game inside Telegram. The experience combines character customisation, career quests, animated rewards, and a persistent game economy. My work spans React and TypeScript interfaces, authentication and payments, Cloudflare Workers APIs, PostgreSQL storage, and localisation. I collaborate with a designer across projects and work with a senior Unity developer on our shared multiplayer codebase. ## Usage Snapshot: 2026-10. Figures supplied by the portfolio owner. - 15k monthly active users - 20M+ spins to date - 30k downloads - 54% D7 retention ## Making the wheel feel responsive A spin is a sequence of interactions: pressing the button, watching the wheel move, seeing the pointer settle, and collecting a reward. The interface needs to make those transitions feel connected while preventing another spin from starting at the wrong moment. I keep the spin controller separate from the page’s presentation. The controller supplies outcomes, balances and available actions; presentation hooks coordinate the wheel, pointer, button and wing. The view combines the pending, spinning and reward states to decide when its controls are busy. The figure below uses the application’s component and authored wheel configuration, with the prize lineup checked against the live app. A local controller cycles through outcomes so you can try the interactions and read the source alongside them. ### Interactive demo: Holy Wheel Live prize lineup, checked October 2026. Spin, change the multiplier, and collect rewards; outcomes cycle locally and balances are simulated. - [Try the demo](https://tesselpunt.com/projects/pebbler-telegram#demo-holy-wheel) - [Component — HolyWheelPage.tsx](https://tesselpunt.com/demo-code/holy-wheel/HolyWheelPage.tsx-72d25040ea53.md): Application source - [Styles — HolyWheelPage.scss](https://tesselpunt.com/demo-code/holy-wheel/HolyWheelPage.scss-b6dca21aba11.md): Application source - [Demo setup — Wheel.tsx](https://tesselpunt.com/demo-code/holy-wheel/Wheel.tsx-0b4c17d1d9e5.md): Portfolio adapter & sample data - [Configuration — holyWheelConfig.ts](https://tesselpunt.com/demo-code/holy-wheel/holyWheelConfig.ts-abcc8f9ec710.md): Application source - [Sound controls — AudioControls.tsx](https://tesselpunt.com/demo-code/holy-wheel/AudioControls.tsx-7e5d42653a0d.md): Portfolio adapter & sample data ## Bringing career progression to life The career page brings a character, a layered environment and several skill tracks into one scene. Completing a quest changes the player’s progress, but it also needs a visible response from the world around them. JobScene connects quest completion with progress indicators, reward flights and character reactions. Scene configuration supplies the environment and its tracks, while the component coordinates their presentation. It also accepts a suspended state so presentation work can pause when the scene is obscured. Here, the Pirate Cook scene runs with the matching outfit, sample quests and a local balance. Completing a quest shows how the progression and feedback fit together. ### Interactive demo: Career scene Explore the Pirate Cook scene and complete the sample quests. Progress stays inside this demo. - [Try the demo](https://tesselpunt.com/projects/pebbler-telegram#demo-career) - [Component — JobScene.tsx](https://tesselpunt.com/demo-code/career/JobScene.tsx-3f55a64ccc38.md): Application source - [Styles — JobScene.scss](https://tesselpunt.com/demo-code/career/JobScene.scss-b15aea5b1a69.md): Application source - [Demo setup — Career.tsx](https://tesselpunt.com/demo-code/career/Career.tsx-ed7b9a198f30.md): Portfolio adapter & sample data - [Status indicators — CareerStatusIndicators.tsx](https://tesselpunt.com/demo-code/career/CareerStatusIndicators.tsx-fcd9c60b4f62.md): Application source - [Sound controls — AudioControls.tsx](https://tesselpunt.com/demo-code/career/AudioControls.tsx-7e5d42653a0d.md): Portfolio adapter & sample data ## Making the Gnome your own The changing room brings clothing categories, skin colors and a live character preview together. Equipping a hat, shirt or pair of glasses updates the animated Gnome while the inventory keeps the current selection visible. The Spine character layers clothing, facial expressions and body animation. Its emote recordings follow the animation clock, and the shared sound controls stop playback when a preview is muted or hidden. I created many of Pebbler Telegram’s sound effects using ElevenLabs’ Sound Effects web app, then integrated them into the game’s interactions and animations. Try the changing room below with all 46 clothing items available locally. Choose an emote to see and hear the character react, or open the same changing room from the Career demo and take your outfit back into the scene. ### Interactive demo: Gnome changing room Choose clothing, change skin color and play an emote with its original sound. All 46 wardrobe items are available here; selections stay inside this demo. - [Try the demo](https://tesselpunt.com/projects/pebbler-telegram#demo-changing-room) - [Component — GnomeChangingRoomPage.tsx](https://tesselpunt.com/demo-code/changing-room/GnomeChangingRoomPage.tsx-8f1fcd66d458.md): Application source - [Styles — GnomeChangingRoomPage.scss](https://tesselpunt.com/demo-code/changing-room/GnomeChangingRoomPage.scss-a594942b4fd4.md): Application source - [Emote preview — ChangingRoom.tsx](https://tesselpunt.com/demo-code/changing-room/ChangingRoom.tsx-b65a1747fda7.md): Portfolio adapter & sample data - [Local wardrobe — gnome-customization.ts](https://tesselpunt.com/demo-code/changing-room/gnome-customization.ts-817e4797df4a.md): Portfolio adapter & sample data - [Sample data — gnome-fixtures.ts](https://tesselpunt.com/demo-code/changing-room/gnome-fixtures.ts-ea2d7297f067.md): Portfolio adapter & sample data - [Sound controls — AudioControls.tsx](https://tesselpunt.com/demo-code/changing-room/AudioControls.tsx-7e5d42653a0d.md): Portfolio adapter & sample data ## Designing the purchase flow The bundle dialog has to communicate the offer at a glance: what is included, what it costs, and whether it is still available. The artwork is only one part of that job. The component also handles localised amounts, countdowns, purchase limits, busy states and error messages. I keep checkout ownership outside the dialog. It receives availability and purchase state, and calls an action supplied by its owner. Invoice creation, duplicate prevention and reward grants belong to that checkout flow. This lets the same presentation run in isolated scenarios without creating a real payment. The example below simulates a successful purchase. It changes the interface state locally and never creates an invoice. ### Interactive demo: VIP bundle Try the VIP bundle offer. The checkout is simulated; no invoice or payment is created. - [Try the demo](https://tesselpunt.com/projects/pebbler-telegram#demo-bundle-offer) - [Component — BundleOfferDialog.tsx](https://tesselpunt.com/demo-code/bundle-offer/BundleOfferDialog.tsx-a94a831fc6e9.md): Application source - [Styles — BundleOfferDialog.scss](https://tesselpunt.com/demo-code/bundle-offer/BundleOfferDialog.scss-fcb8b556ac0b.md): Application source - [Demo setup — Bundle.tsx](https://tesselpunt.com/demo-code/bundle-offer/Bundle.tsx-6c65ca71b638.md): Portfolio adapter & sample data - [Sound controls — AudioControls.tsx](https://tesselpunt.com/demo-code/bundle-offer/AudioControls.tsx-7e5d42653a0d.md): Portfolio adapter & sample data ## Protecting reward withdrawals from abuse An earlier version of Pebbler offered crypto rewards and attracted automated accounts. I built a verification workflow using AWS Rekognition Face Liveness, connecting the React camera interface with backend checks at withdrawal. I compared new enrolments with existing face vectors to detect the same person across multiple accounts, sending matches to review before permitting withdrawal. I also implemented periodic re-verification at randomised three-to-six-month intervals, triggered when a player next attempts a withdrawal. The new scan must match the enrolled face before withdrawal can proceed. The interface handles consent, camera guidance and retries, while the server evaluates provider results and authorises the withdrawal. After rollout, I observed no further reward payouts to fully automated bots. Human-operated farms remained possible, but requiring live participation and recurring identity checks increased the effort needed to maintain abusive accounts. ## The database behind the game Pebbler stores its persistent game state in PostgreSQL. The DBML diagram below shows the database tables and their relationships alongside the interfaces they support. ### Pebbler database schema Explore the tables and relationships in the DBML representation of the database. - [Interactive diagram](https://dbdiagram.io/e/6a8dabf3fd15a881e5f38112/6abf0608abcc87fb7acd652e) - [Database schema — PbDbV2.dbml](https://tesselpunt.com/demo-code/pebbler-database/PbDbV2.dbml-bcb645e051f8.md): Pebbler database schema ## Testing the states around the happy path I use Storybook to work through purchase states, localised layouts and reward presentation in isolation. Vitest browser tests cover checkout retries, resumable orders, and responses that arrive after navigation. These views also sit on shared foundations. I maintain authentication libraries and a typed React state library used across our own applications, so recurring integration and state patterns can be maintained in one place. - [Telegram authentication](https://tesselpunt.com/projects/tg-auth) - [Typed React state](https://tesselpunt.com/projects/provider-kit) ## From design to implementation I work with a designer across these projects. To make the handoff more useful for AI coding tools, I built Figma Agent Context: a plugin that exports geometry, design variables, assets and visual references together. I used that workflow to build nearly all of the Holy Wheel page, the bundle offer dialogs, and most of the career page. The interfaces above are examples of where the tool was used in production work. - [How Figma Agent Context works](https://tesselpunt.com/projects/figma-context) ## Screenshots ### Holy Wheel A playful reward wheel with animated feedback, multipliers, and a reward collection flow. Image description: Pebbler Holy Wheel screen with a colourful prize wheel and a large Spin button - [Image](https://tesselpunt.com/media/holy-wheel-large.webp) - [Screenshot page](https://tesselpunt.com/projects/pebbler-telegram/gallery/holy-wheel) ### A career, one quest at a time The Pirate Cook scene: character animation, an animated ship galley, and three skill tracks. Image description: Pebbler career scene showing a gnome in a pirate cook outfit in a ship galley above three progression cards - [Image](https://tesselpunt.com/media/career-large.webp) - [Screenshot page](https://tesselpunt.com/projects/pebbler-telegram/gallery/career) ### A clear offer in a busy world The VIP bundle dialog brings pricing, rewards, availability, and purchase feedback together. Image description: VIP bundle dialog showing included coins, energy, clothing and spins, with a purchase button - [Image](https://tesselpunt.com/media/bundle-offer-large.webp) - [Screenshot page](https://tesselpunt.com/projects/pebbler-telegram/gallery/bundle-offer) ### Make the character yours An inventory with clear owned, equipped, and locked states, alongside a live character preview. Image description: Pebbler wardrobe interface showing a character and a grid of hats in owned and locked states - [Image](https://tesselpunt.com/media/wardrobe-large.webp) - [Screenshot page](https://tesselpunt.com/projects/pebbler-telegram/gallery/wardrobe) --- # Pebbler® on World - [Case study](https://tesselpunt.com/projects/pebbler-world) A consumer-facing micro-task app connecting people with surveys, A/B comparisons, and play-and-earn quests. Small tasks. Meaningful scale. Role: End-to-end product engineering Period: 2024 — present Technologies: React, TypeScript, PostgreSQL, Workers - [Live application](https://world.org/ecosystem/app_0e3f2e07cf3fb2e43fdddbb73d21d355) I built and operate a micro-task application in World App, where people earn crypto rewards by completing surveys, making A/B comparisons, and playing game quests. The app has reached a peak of approximately 140,000 weekly active users. The product combines localised React interfaces with Cloudflare Workers APIs, PostgreSQL, KV, and R2. I own the journey from interface implementation to production operations. ## Usage Snapshot: 2026-10. Figures supplied by the portfolio owner. - ~25k weekly active users - 4.8M surveys completed - 21.3M A/B tests completed - ~1M downloads - ~150k weekly opens - 4.8 app rating - [World activity source](https://www.miniapps.world/) ## Support and programmes ### World Foundation — Multiple grants Multiple grants from the World Foundation helped support Pebbler on World. - [Programme](https://foundation.world.org/) ### PostHog — Startup program Supported through the PostHog startup program. - [Programme](https://posthog.com/startups) ### Cloudflare — Startup program Supported through the Cloudflare startup program. - [Programme](https://www.cloudflare.com/startups/) ### DigitalOcean — Hatch program Supported through DigitalOcean’s Hatch program. - [Programme](https://www.digitalocean.com/startups) ### Google Cloud — Program support Received program support from Google Cloud. - [Programme](https://cloud.google.com/startup) ## Helping businesses ask useful questions Pebbler also includes the tools to create the research. A business can start from a survey template, enter the subject and options, and inspect the respondent experience before publishing. The form widgets share validation and field state. A separate template compiler turns those values into the survey definition, keeping the authoring interface and the respondent interface consistent. Pricing comparisons and slogan feedback exercise different field types without duplicating the whole flow. Try the application’s template fields below, then open the survey preview. The surrounding controls use local sample data; publication, translation requests and reward allocation are outside this preview. ### Interactive demo: Survey builder Edit a pricing or slogan template, then preview the survey. The editor and survey compiler come from the application; no survey is published. - [Try the demo](https://tesselpunt.com/projects/pebbler-world#demo-survey-builder) - [Template editor — AbPricingTemplateContent.tsx](https://tesselpunt.com/demo-code/survey-builder/AbPricingTemplateContent.tsx-877622c59ff2.md): Application source - [Form state — TemplateContentContext.tsx](https://tesselpunt.com/demo-code/survey-builder/TemplateContentContext.tsx-2cdc04634da5.md): Application source - [Survey compiler — abPricingTemplate.ts](https://tesselpunt.com/demo-code/survey-builder/abPricingTemplate.ts-32ddac14ae90.md): Application source - [Demo setup — Scene.tsx](https://tesselpunt.com/demo-code/survey-builder/Scene.tsx-1299f20e0fe6.md): Portfolio adapter & sample data - [Preview shell — Preview.tsx](https://tesselpunt.com/demo-code/survey-builder/Preview.tsx-bd1ce946a382.md): Portfolio adapter & sample data ## Bridging web and Unity The play-and-earn feature launches specific levels in licensed Unity WebGL games through signed deep links and a bidirectional JavaScript/C# bridge. The illustrated map gives each day a place in the world. Selecting a marker reveals its quests, while the details drawer explains the objective, status and reward before a player starts a game. Explore the application’s map below with local sample data. The preview fixes today to August 12, 2026, and includes two sample history days. Game launches, authentication and reward processing are outside this demo. ### Interactive demo: Play & Earn map Scroll the map, select a day and open a quest to inspect its rewards. June 27 is today in this preview; June 25 and 26 show sample history. Game launching and rewards stay outside the demo. - [Try the demo](https://tesselpunt.com/projects/pebbler-world#demo-play-earn-map) - [Map — GaeMapPage.tsx](https://tesselpunt.com/demo-code/play-earn-map/GaeMapPage.tsx-22dd33fc0707.md): Application source - [Styles — GaeMapPage.scss](https://tesselpunt.com/demo-code/play-earn-map/GaeMapPage.scss-1addc28cc697.md): Application source - [Quest list — GaeMapQuestList.tsx](https://tesselpunt.com/demo-code/play-earn-map/GaeMapQuestList.tsx-071c8ea4b951.md): Application source - [Quest details — GaeQuestDetailsContent.tsx](https://tesselpunt.com/demo-code/play-earn-map/GaeQuestDetailsContent.tsx-f1474762fa77.md): Application source - [Demo setup — Scene.tsx](https://tesselpunt.com/demo-code/play-earn-map/Scene.tsx-6bdb4fa64eed.md): Portfolio adapter & sample data - [Sample data — fixtures.ts](https://tesselpunt.com/demo-code/play-earn-map/fixtures.ts-d93770ac787f.md): Portfolio adapter & sample data - [Local transport — local-services.ts](https://tesselpunt.com/demo-code/play-earn-map/local-services.ts-f3d1dd315e48.md): Portfolio adapter & sample data - [Frame viewport — useFrameViewport.ts](https://tesselpunt.com/demo-code/play-earn-map/useFrameViewport.ts-ad75b4f5a195.md): Portfolio adapter & sample data ## Reliable reward processing Quest-state isolation, completion retries, and transactional reward processing with duplicate-claim handling coordinate the game and application flows. ## Activity with context The figures shown here describe usage in an incentivised micro-task product. Surveys and A/B tasks are completed activities; they are not revenue or retention metrics. ## Screenshots ### From a question to a survey The pricing template editor, with a fictional coffee business and sample price options. Image description: Pebbler survey builder showing business name, product and pricing options Interactive portfolio preview using application components and sample data. - [Image](https://tesselpunt.com/media/survey-builder-large.webp) - [Screenshot page](https://tesselpunt.com/projects/pebbler-world/gallery/survey-builder) ### A daily quest, on the map The illustrated Play & Earn map with selectable days, quest history and reward details. Image description: Pebbler Play & Earn city map with daily markers and the Complete Level 8 quest Interactive portfolio preview using application components and local sample data. - [Image](https://tesselpunt.com/media/play-earn-map-large.webp) - [Screenshot page](https://tesselpunt.com/projects/pebbler-world/gallery/play-earn-map) --- # Pebbler® Admin Dashboard - [Case study](https://tesselpunt.com/projects/pebbler-admin) An operations console with AI workflows for localising game headers and producing quest artwork. The tools behind the product. Role: Workflow design & full-stack engineering Period: 2025 — present Technologies: React, TypeScript, OpenAI, Gemini, Storybook I built the React and TypeScript operations console used to manage Pebbler’s content and workflows. Its tools cover content releases, campaigns, asset catalogues and AI image production. Two recurring production tasks shaped Image Studio: adapting illustrated headers for different locales, and creating consistent quest cards for Pebbler Telegram. Bringing recipes, references, review and exports into one workspace made both tasks quicker and easier in daily use. ## Quest artwork, with room to iterate Boards turn a list of quests into a review workspace. Each card keeps its prompt, ordered style references, candidate history and chosen winner. I can revise one quest and generate more alternatives while keeping its position and previous selection. The saved Oil Rig Worker board contains 14 quests, 92 attempts and 14 selected winners. “Survive Someone Else’s Shortcut” went through 12 candidates; its prompt was revised to explicitly request a square quest image, and candidate 9 was chosen. Every candidate retains the inputs used at the time, even after the current draft changes. This preview uses the production board and comparison components with three recorded quests. It includes four candidates each for “Dodge the Swinging Pipe” and “Watch the Shakers”, plus candidates 7–12 for “Survive Someone Else’s Shortcut”. New generation is simulated with that artwork; the code browser identifies the application source and the portfolio controller. ### Interactive demo: Image Studio · Boards Compare recorded Oil Rig Worker candidates, inspect their original inputs and choose winners. Generation replays existing artwork; references stay local and export saves a selection manifest. - [Try the demo](https://tesselpunt.com/projects/pebbler-admin#demo-image-studio) - [Workspace — StudioBoardWorkspace.tsx](https://tesselpunt.com/demo-code/image-studio/StudioBoardWorkspace.tsx-f23dda5fcb4d.md): Application source - [Styles — ImageStudioBoardsPage.scss](https://tesselpunt.com/demo-code/image-studio/ImageStudioBoardsPage.scss-6da897bac616.md): Application source - [Demo setup — Boards.tsx](https://tesselpunt.com/demo-code/image-studio/Boards.tsx-7150f6e82c2c.md): Portfolio adapter & sample data - [Draft & recovery — useImageStudioBoards.ts](https://tesselpunt.com/demo-code/image-studio/useImageStudioBoards.ts-9cb8b251deed.md): Application source - [Recorded examples — recordings.ts](https://tesselpunt.com/demo-code/image-studio/recordings.ts-5b60a4d9992d.md): Recorded data & portfolio adapter ## One recipe, twenty locale variants Illustrated game headers combine lettering with a specific art style. Create images lets me supply the source artwork and a key/value list of translated text, then expand one prompt and filename template into a batch. The locale key names the exported file; the supplied value becomes the text in the generation prompt. The recorded VIP bundle batch produced 20 locale variants from one reference, using GPT Image 2 with transparent WebP output. The gallery makes it practical to inspect different scripts and longer strings together, then retry an individual output without rerunning successful images. The translations are supplied by the operator; this workflow generates the artwork. The preview below reuses the application’s variable editor and recipe validation. It shows the original outputs, expanded inputs and a simulated failure/retry scenario. Edited recipes can be inspected without presenting the recorded artwork as newly generated results. ### Interactive demo: Image Studio · Localisation Explore the recorded VIP header batch, edit the key/value recipe and inspect expanded prompts and filenames. Replay and retry are simulated; downloads contain the original recorded images. - [Try the demo](https://tesselpunt.com/projects/pebbler-admin#demo-image-localisation) - [Variable editor — ImageStudioVariableEditor.tsx](https://tesselpunt.com/demo-code/image-localisation/ImageStudioVariableEditor.tsx-5042d0928757.md): Application source - [Draft validation — imageStudioDraft.ts](https://tesselpunt.com/demo-code/image-localisation/imageStudioDraft.ts-18c85bac0d45.md): Application source - [Demo controller — Localisation.tsx](https://tesselpunt.com/demo-code/image-localisation/Localisation.tsx-a5bf01d42146.md): Portfolio adapter & sample data - [Recorded examples — recordings.ts](https://tesselpunt.com/demo-code/image-localisation/recordings.ts-5b60a4d9992d.md): Recorded data & portfolio adapter - [Preview styles — scene.scss](https://tesselpunt.com/demo-code/image-localisation/scene.scss-ebac1a29de37.md): Portfolio adapter & sample data ## Keep editing while work is in flight A save response can arrive after the operator has already made another edit. The board controller compares the submitted draft with the latest local draft, so an older response cannot replace newer work. Generation waits for the current draft to be saved and captures its inputs separately from later edits. Local draft persistence and revision checks protect work across reloads and server conflicts. Candidate history keeps the original prompt, references and model settings, so repeating an earlier attempt does not silently use the current draft. ## Recovery by design Paid generation needs careful recovery. The worker records a request’s submission state before calling a provider. Explicit rate limits can be retried with backoff; an uncertain response is held for inspection instead of automatically risking a duplicate paid request. A generation receipt also lets the board recover from a lost server response without creating another run. This mattered in a recorded “KEEP PLAYING!” localisation run: the Vietnamese output was cancelled and then failed across several attempts before its sixth attempt completed. The history retained those outcomes and the successful image rather than hiding the failed work. Focused tests cover draft restoration, attempt selection, progress reporting and stable board state. Browser scenarios exercise edits during an in-flight save and recovery after a lost generation response. Storybook scenarios cover candidate review and its interface states. ## Built for daily operation The studio lives alongside content releases, campaigns and asset catalogues. Production exports preserve the generated image bytes and include a manifest of the selected attempts or winners, connecting creative review to the files that the applications use. The benefit here is practical: faster localisation of illustrated headers and faster creation of quest artwork, with recipes and candidate history ready for the next content release. ## Screenshots ### Image Studio workspace Real Oil Rig Worker quest artwork, ordered references and recorded candidate comparisons. Image description: Pebbler Image Studio with image candidate cards and a prompt editor Application workspace with recorded artwork and simulated generation. - [Image](https://tesselpunt.com/media/image-studio-large.webp) - [Screenshot page](https://tesselpunt.com/projects/pebbler-admin/gallery/image-studio) --- # Bridge - [Case study](https://tesselpunt.com/projects/bridge) Browser-based file sharing, with direct WebRTC transfers and cloud upload flows. Complex transfers. A clear experience. Role: Frontend & transfer engineering Period: 2025 — 2026 Technologies: React, TypeScript, Cloud uploads - [Live application](https://world.org/ecosystem/app_443bd39e83f7ab076b200e630c70c772) I implemented browser file-sharing flows for direct WebRTC transfers and cloud uploads. The interface surfaces progress while the transfer implementation handles changing conditions. This work includes adaptive chunking, buffer-based flow control, integrity checks, and persistent download state. ## Usage Snapshot: 2026-10. Figures supplied by the portfolio owner. - 193k downloads - 3.4k weekly active users - 17k weekly sessions - 4.3 app rating - [World activity source](https://www.miniapps.world/) ## Control the flow Adaptive chunks and buffer-based backpressure help coordinate reading files with the transport’s ability to send them. ## Make progress understandable The current cloud-sharing flow moves from file selection to expiry, anonymity and an optional message. Upload progress leads into a shareable link and QR code, with completed transfers accessible from history. This preview uses the production settings, upload-progress and completion drawers, file lists and overview components. A local transport replaces presigning, uploads and finalization; no files reach the production backend. Try the full flow, then open the completed transfer in history. ### Interactive demo: Bridge file sharing Select sample files, set an expiry and message, then upload and open the link or QR code. Uploads stay local; shared demo links reopen a sample transfer. - [Try the demo](https://tesselpunt.com/projects/bridge#demo-bridge-sharing) - [Share settings — CloudSharingSettingsContent.tsx](https://tesselpunt.com/demo-code/bridge-sharing/CloudSharingSettingsContent.tsx-1cfa41608e2c.md): Application source - [Styles — CloudSharingSettingsContent.scss](https://tesselpunt.com/demo-code/bridge-sharing/CloudSharingSettingsContent.scss-9cf1a023b475.md): Application source - [Upload flow — CloudSharingUploadContent.tsx](https://tesselpunt.com/demo-code/bridge-sharing/CloudSharingUploadContent.tsx-6acbc2875ee2.md): Application source - [Share link & QR — ShareableCodeBox.tsx](https://tesselpunt.com/demo-code/bridge-sharing/ShareableCodeBox.tsx-ce628e75ddca.md): Portfolio adapter & sample data - [Demo setup — Scene.tsx](https://tesselpunt.com/demo-code/bridge-sharing/Scene.tsx-7e422f90817a.md): Portfolio adapter & sample data - [Local transport — local-transfer.ts](https://tesselpunt.com/demo-code/bridge-sharing/local-transfer.ts-38f413ace818.md): Portfolio adapter & sample data ## Think beyond the happy path Integrity checks and persistent download state address correctness and continuity in a browser environment. ## Screenshots ### A shared portal The production cloud-sharing flow: select files, configure expiry, upload and share. Image description: Bridge cloud-sharing interface with expiry, anonymity and an optional message Interactive portfolio preview using application components and sample data. - [Image](https://tesselpunt.com/media/bridge-sharing-large.webp) - [Screenshot page](https://tesselpunt.com/projects/bridge/gallery/bridge-sharing) --- # Asset One - [Case study](https://tesselpunt.com/projects/asset-one) A mobile portfolio interface for exploring balances, asset prices and transaction history. Every asset. One clear view. Role: Interface design & frontend engineering Period: 2025 Technologies: React, TypeScript, Chart.js, State management Asset One brings holdings and market data into a mobile interface. I built the React views for portfolio charts, asset selection, transaction history and currency conversion. The chart connects time ranges, price series and portfolio snapshots. Selecting an asset or inspecting a point in time changes what the interface presents, while shared state keeps the underlying data consistent. ## Make a dense dataset feel approachable The chart component renders portfolio snapshots and individual asset prices through the same interaction model. Range selection changes the series; dragging across the chart reveals values at a particular point in time. The preview uses the application’s InstrumentChart, asset picker and balance display, backed by deterministic sample data. It needs no wallet connection and does not request current market prices. ### Interactive demo: Asset One Explore time ranges, choose an asset and drag across its chart. Balances and prices are fictional samples; no wallet connection is required. - [Try the demo](https://tesselpunt.com/projects/asset-one#demo-asset-one) - [Chart — InstrumentChart.tsx](https://tesselpunt.com/demo-code/asset-one/InstrumentChart.tsx-8d29cf6af7f9.md): Application source - [Styles — InstrumentChart.scss](https://tesselpunt.com/demo-code/asset-one/InstrumentChart.scss-130aa1f364f4.md): Application source - [Demo setup — Scene.tsx](https://tesselpunt.com/demo-code/asset-one/Scene.tsx-da1ab081d6b8.md): Portfolio adapter & sample data - [Sample data — fixtures.ts](https://tesselpunt.com/demo-code/asset-one/fixtures.ts-7fdd1a361e1b.md): Portfolio adapter & sample data ## Keep data separate from presentation Typed slices describe portfolio snapshots, price histories and currency preferences. The chart consumes those slices rather than owning data fetching, which also makes it possible to run the interface in isolation. Alongside the chart, the application includes transaction views and conversion tools. This demo focuses on the chart and selection interactions. ## Screenshots ### Your portfolio, in perspective The Asset One chart interface with deterministic sample holdings and price history. Image description: Asset One portfolio chart with time ranges, asset selection and a sample balance Interactive portfolio preview using application components and sample data. - [Image](https://tesselpunt.com/media/asset-one-large.webp) - [Screenshot page](https://tesselpunt.com/projects/asset-one/gallery/asset-one) --- # Figma Agent Context - [Case study](https://tesselpunt.com/projects/figma-context) A Figma plugin that gives coding agents structured design context and visual references. Shorten the distance between design and code. Role: Tool design & TypeScript development Period: 2026 Technologies: Figma Plugin API, TypeScript, React, SCSS I built a TypeScript Figma plugin that normalises design geometry and exports compact React/SCSS context, variable bindings, assets, and visual references. I used the output with AI coding agents to implement nearly all of Pebbler Telegram’s HolyWheel screen, its bundle offer dialogs, and most of its career page. The shipped interfaces below show where the tool was used. ## Useful context The export pairs structured layout and style information with the assets and images needed to interpret the design. ## Used in production work HolyWheel and the bundle dialogs provide concrete examples of the design-to-code workflow in the Pebbler application. ### Holy Wheel A playful reward wheel with animated feedback, multipliers, and a reward collection flow. Image description: Pebbler Holy Wheel screen with a colourful prize wheel and a large Spin button - [Image](https://tesselpunt.com/media/holy-wheel-large.webp) - [Screenshot page](https://tesselpunt.com/projects/figma-context/gallery/holy-wheel) ### A clear offer in a busy world The VIP bundle dialog brings pricing, rewards, availability, and purchase feedback together. Image description: VIP bundle dialog showing included coins, energy, clothing and spins, with a purchase button - [Image](https://tesselpunt.com/media/bundle-offer-large.webp) - [Screenshot page](https://tesselpunt.com/projects/figma-context/gallery/bundle-offer) ### A career, one quest at a time The Pirate Cook scene: character animation, an animated ship galley, and three skill tracks. Image description: Pebbler career scene showing a gnome in a pirate cook outfit in a ship galley above three progression cards - [Image](https://tesselpunt.com/media/career-large.webp) - [Screenshot page](https://tesselpunt.com/projects/figma-context/gallery/career) ## Screenshots ### Holy Wheel A playful reward wheel with animated feedback, multipliers, and a reward collection flow. Image description: Pebbler Holy Wheel screen with a colourful prize wheel and a large Spin button - [Image](https://tesselpunt.com/media/holy-wheel-large.webp) - [Screenshot page](https://tesselpunt.com/projects/figma-context/gallery/holy-wheel) ### A clear offer in a busy world The VIP bundle dialog brings pricing, rewards, availability, and purchase feedback together. Image description: VIP bundle dialog showing included coins, energy, clothing and spins, with a purchase button - [Image](https://tesselpunt.com/media/bundle-offer-large.webp) - [Screenshot page](https://tesselpunt.com/projects/figma-context/gallery/bundle-offer) ### A career, one quest at a time The Pirate Cook scene: character animation, an animated ship galley, and three skill tracks. Image description: Pebbler career scene showing a gnome in a pirate cook outfit in a ship galley above three progression cards - [Image](https://tesselpunt.com/media/career-large.webp) - [Screenshot page](https://tesselpunt.com/projects/figma-context/gallery/career) --- # Shared libraries - [Case study](https://tesselpunt.com/projects/libraries) Authentication and React state-management packages reused across Bridge Labs applications. Three packages. Three engineering stories. Role: Library design & maintenance Period: 2024 — present Technologies: React, TypeScript, Zod, Authentication I maintain shared libraries for recurring problems across our applications. Their consumers are primarily applications maintained within Bridge Labs. Explore the authentication case studies and the React state library below. Each project has its own page for the problem, implementation, and decisions behind the package. ## Authentication tg-auth connects verified Telegram launch data with application sessions. world-auth connects World wallet proofs with session-bound challenges and backend verification. Their case studies explain the different verification flows and the session mechanics they share. - [tg-auth case study](https://tesselpunt.com/projects/tg-auth) - [world-auth case study](https://tesselpunt.com/projects/world-auth) ## provider-kit A React/TypeScript state library with typed per-slice hooks, functional updates, separate slice contexts, and runtime Zod validation. It handles shared client state rather than serving as a server-data cache. - [provider-kit project](https://tesselpunt.com/projects/provider-kit) --- # Voiced Gnome - [Case study](https://tesselpunt.com/projects/voiced-gnome) Eighteen animated characters with individual voices and backstories. Browse the cast, hear their introductions and ask a question. Giving an animated character a voice—and keeping the interaction responsive. Role: Frontend, animation, voice integration & API Period: 2026 Technologies: React, TypeScript, ElevenLabs, Spine, Cloudflare - [Public repository](https://github.com/bridge-applications/voiced-gnome) I adapted the Spine character and wardrobe from Pebbler for Telegram into a standalone voice demo. Each of the 18 characters has its own outfit, skin palette, fictional backstory and stock voice. The browser coordinates conversation state, speech playback, mouth movements and expressive gestures. ## Meet the characters Use the arrows to choose a character. About me plays a recorded introduction with mouth cues and timed emotes; typed and spoken conversations use ElevenLabs. The demo loads on request, and microphone permission is requested only after choosing Speak. - [Try Voiced Gnome](https://tesselpunt.com/projects/voiced-gnome#try-it) - [Conversation — CharacterExperience.tsx](https://tesselpunt.com/demo-code/voiced-gnome/CharacterExperience.tsx-be56949d3d83.md): Session setup, stale-response guards, interruption handling and validated expression tools. - [Lip sync — liveSpeech.ts](https://tesselpunt.com/demo-code/voiced-gnome/liveSpeech.ts-dcc9a4297002.md): Character alignment drives estimated mouth cues, with interruption guards and a return to rest during silence. - [Worker — quota.ts](https://tesselpunt.com/demo-code/voiced-gnome/quota.ts-f4a7c5f94d01.md): Transactional SQLite reservations reject duplicates and enforce allowances before any provider request. - [Tests — quota-runtime.test.ts](https://tesselpunt.com/demo-code/voiced-gnome/quota-runtime.test.ts-5ca5b81613b0.md): Real Workers runtime tests race 20 requests for the final allowance and check duplicates, concurrency and persistence after restart. ## Making speech and motion agree The face uses nine mouth attachments. Recorded introductions follow cues derived from the actual audio, against the playback clock. Live speech combines provider character alignment, a pronunciation lookup and an amplitude fallback. Driving movement from playback time keeps buffering from advancing the mouth ahead of the voice. The selected gnome stays large while neighbouring characters preview the next outfits. Wardrobe textures load on demand. Character changes, interruptions and cancelled requests clear the previous speech and gesture state so an old reply cannot take over the newly selected character. ## Expressive tools with a small boundary The agent can request an expression or emote through validated tools. The browser accepts known actions and applies them to the existing rig. Typed replies are split into bounded speech requests; the Worker selects the character voice and speech model from the shared catalog. Shared Zod schemas connect the client, Worker and types package. ## Protecting a public voice demo Paid requests require server-validated Turnstile, a signed visitor capability and an idempotency key. SQLite Durable Objects reserve per-visitor and daily allowances before generation, reject duplicate or overlapping work, and preserve quotas across restarts. Static browsing and introductions remain available when generation is paused or an allowance is exhausted. Live signed URLs can be reused, so URL admission counts are not voice-minute accounting. The provider enforces call duration, concurrency and daily limits independently. A stricter per-visitor voice-minute allowance would need a server-owned relay. The repository documents that limitation alongside runtime tests for concurrent quota claims and persistence. - [Controls, tests and tradeoffs](https://github.com/bridge-applications/voiced-gnome/blob/main/docs/anti-abuse.md) --- # tg-auth - [Case study](https://tesselpunt.com/projects/tg-auth) Typed Telegram authentication, server verification, and encrypted application sessions shared between browser clients and backend services. From a Telegram launch to a reliable application session. Role: Library design, implementation & maintenance Technologies: Telegram, TypeScript, Authentication, Zod - [Public repository](https://github.com/bridge-applications/tg-auth-public) I maintain the authentication package used by Pebbler for Telegram and its backend services. It connects Telegram Mini App launch data with server verification, encrypted application credentials, and the browser session lifecycle. The public edition brings that shared browser and server code into a standalone, documented package. Its scope is identity verification and credential management; account lookup, permissions, and business rules remain with the application. ## Used by - [Pebbler® for Telegram](https://tesselpunt.com/projects/pebbler-telegram) ## Package example Verified Telegram launch data → application credentials ```ts const launch = await auth .verifyInitData(rawInitData); if ( !launch.valid || !launch.user ) { throw new Error('Rejected'); } const account = await accounts .findAllowedTelegramUser( launch.user.id, ); const credentials = await auth .genAccessToken({ userId: account.id, telegramId: String(launch.user.id), }); ``` ## Giving authentication a shared home A Telegram launch is only the beginning of an application session. The backend needs to verify the launch data, connect the identity to an account, and issue credentials. The browser then needs to reuse those credentials, refresh them, and clear them when the user signs out. Keeping these recurring mechanics in a package gives the client and server a consistent contract. Zod schemas validate requests, responses, and token claims at runtime while preserving their TypeScript types. Application-specific account decisions stay outside that contract, so reusing authentication does not require reusing an entire backend. ## Making the server the source of trust The browser submits Telegram’s raw initData. A user object read from the browser, including initDataUnsafe, is not sufficient evidence of identity. The server verifies the protocol proof before an application can use the resulting identity to find an account. The verifier supports bot-token HMAC and Telegram’s Ed25519 signatures. Parsing matters as much as the cryptographic check: duplicate decoded keys, malformed encoding, and ambiguous fields are rejected. Freshness checks bound the lifetime of launch data, with a small allowance for clock differences. ### Resolve an account after verification Illustrative integration excerpt. The configured verifier supplies the trusted Telegram identity; the application supplies account lookup and its claim schema. ```ts const launch = await auth.verifyInitData(rawInitData); if (!launch.valid || !launch.user) { throw new Error('Telegram sign-in rejected'); } const account = await accounts.findAllowedTelegramUser(launch.user.id); const credentials = await auth.genAccessToken({ userId: account.id, telegramId: String(launch.user.id), }); ``` - [Server integration reference](https://github.com/bridge-applications/tg-auth-public#server-verification-and-credentials) ## Balancing repeated launches and replay protection Verified launch data is a bearer assertion: possession of a valid assertion is enough to present it again while it remains fresh. Verification alone does not make it single-use. Telegram can also supply the same assertion more than once during a launch, so rejecting every repeated exchange would affect legitimate resume behavior. Single-use verification is therefore an explicit option. When enabled, an atomic store consumes a fingerprint of the authenticated fields. Equivalent query encodings and different supported proof paths map to the same fingerprint. A repeated launch resumes through its refresh cookie rather than exchanging the same assertion again. The application chooses its freshness window and replay policy for the experience it supports. - [Replay protection and trust boundaries](https://github.com/bridge-applications/tg-auth-public/blob/main/docs/architecture.md) ## Keeping refresh and logout predictable Several API requests can discover an expired access token at the same time. The browser client shares one pending refresh request instead of creating a separate exchange for each caller. Valid access credentials stay in memory by default; the refresh credential stays in an HttpOnly cookie. Another challenge is a response arriving after logout. Each credential operation belongs to a session generation. Signing out, resetting, or reinitializing the client invalidates pending commits, so a delayed response cannot restore the old local session. This protects browser state; the backend still owns session closure and token verification. ### Reuse the browser session for an API request After client initialization and session bootstrap, concurrent callers reuse a valid credential or share the pending refresh request. ```ts import { TelegramAuthClient } from '@bridge-applications/tg-auth/client'; const session = await TelegramAuthClient.getAccessTokenSilently(); const response = await fetch('/api/account', { headers: { Authorization: `Bearer ${session.accessToken}` }, }); ``` - [Browser session reference](https://github.com/bridge-applications/tg-auth-public#browser-session) ## Keeping credentials within their intended scope Access and refresh tokens use encrypted JWE envelopes with different purposes and scopes for the application, bot, and environment. Verification checks those boundaries as well as expiry and the configured claim schema. A refresh credential cannot serve as an access credential, and a token from one application context cannot silently become valid in another. Shared refresh-group counters support account-wide invalidation across backend instances. Incrementing a group invalidates the existing refresh credentials for that identity. Already issued access tokens retain their original expiry, so their short lifetime bounds the remaining access window. The group counter must be durable: deleting it can restore authority to an older group-zero credential. ## From internal reuse to a public package The internal package is used by Pebbler for Telegram. The public edition documents integration responsibilities and provides separate client, server, and schema entry points. Browser consumers can import the client without bringing server cryptography or Redis dependencies into their bundle. The repository’s checks cover cryptographic verification, HTTP integration, asynchronous session races, packed-package imports and types, browser dependency isolation, and actual workerd cryptography. These checks make the package’s behavior inspectable; each consuming application still needs to verify its deployed cookies, routes, authorization, and session policy. - [Explore the public repository](https://github.com/bridge-applications/tg-auth-public) - [HTTP integration responsibilities](https://github.com/bridge-applications/tg-auth-public/blob/main/docs/http-integration.md) - [Pebbler for Telegram case study](https://tesselpunt.com/projects/pebbler-telegram) - [World authentication case study](https://tesselpunt.com/projects/world-auth) --- # world-auth - [Case study](https://tesselpunt.com/projects/world-auth) World wallet sign-in, session-bound challenge verification, and encrypted credentials reused across Pebbler, Bridge, and World mini games. One wallet sign-in flow, shared across several applications. Role: Library design, implementation & maintenance Technologies: World, TypeScript, Authentication, SIWE - [Public repository](https://github.com/bridge-applications/world-auth-public) I maintain the World wallet-authentication library used by Pebbler World, Bridge, and five World mini games. It shares the sign-in and session mechanics between browser clients and backend services while each application keeps its own account model and permissions. The public edition exposes the integration contracts around MiniKit wallet commands, Sign-In with Ethereum verification, signed challenges, and encrypted access and refresh credentials. The central question is how a wallet response becomes a session the backend can trust. ## Used by - [Pebbler® on World](https://tesselpunt.com/projects/pebbler-world) - [Bridge](https://tesselpunt.com/projects/bridge) - Brain Bus - UnScrewed! - Picture Jam - Match & Win - Color Jam ## Package example Wallet proof verified against a trusted session challenge ```ts const accepted = await auth .verifySiwe( walletProof, submittedNonce, { expectedNonce: challengeSession.nonce, domain: appConfig.domain, uri: appConfig.uri, chainId: appConfig.chainId, }, ); if (!accepted) { throw new Error('Rejected'); } ``` ## Sharing mechanics across different products A micro-task application, a file-sharing tool, and a mini game use authenticated sessions for different reasons. They still need the same underlying sequence: request a challenge, ask the wallet to sign it, verify the proof, and maintain credentials for subsequent API requests. The package gives that sequence a shared implementation without owning the application’s user database or HTTP routes. MiniKit is supplied by the host application. Separate browser, server, and schema entry points keep server verification dependencies out of the frontend bundle, while runtime schemas make the client/server contract explicit. ## Binding a wallet proof to the application session The backend generates a signed challenge and binds its nonce to a trusted browser session or an HttpOnly challenge cookie. The browser passes that nonce to MiniKit’s wallet-authentication command, then returns the wallet response for server verification. Checking the wallet signature is only one part of accepting the login. The verifier also checks the expected nonce, domain, URI, chain, wallet address, and message timestamps. Expected values come from server configuration and session state, independently of the submitted proof. Successful verification establishes control of a wallet; World ID verification and application permissions are separate decisions. ### Verify against trusted session and application values Illustrative integration excerpt. The configured verifier checks the submitted proof against an independently retrieved session nonce and trusted application configuration. ```ts const accepted = await auth.verifySiwe(walletProof, submittedNonce, { expectedNonce: challengeSession.nonce, domain: appConfig.domain, uri: appConfig.uri, chainId: appConfig.chainId, }); if (!accepted) { throw new Error('Wallet sign-in rejected'); } ``` - [Wallet login reference](https://github.com/bridge-applications/world-auth-public#wallet-login) ## Making challenge consumption atomic A signed challenge proves issuance and expiry, but it does not by itself prevent two backend instances from accepting the same login. Checking whether a nonce has been used and then recording it as a separate operation leaves a race between those requests. Nonce consumption therefore happens atomically in a shared store. Redis uses SET NX with a lifetime tied to the challenge’s expiry; application-supplied stores must provide the same guarantee across processes. Only a valid proof consumes the challenge. Replays, missing stores, and store failures reject the login rather than allowing verification to proceed without that guarantee. - [Challenge and storage contracts](https://github.com/bridge-applications/world-auth-public/blob/main/docs/architecture.md) ## Separating access, refresh, and invalidation After verifying the wallet proof, the application resolves its user record and requests access and refresh credentials. Their encrypted envelopes include a version, an application namespace, and a distinct token purpose. Claims are validated when issued and when verified, so decrypted data is still checked against the application’s schema. An account-wide refresh-group counter provides invalidation across backend instances. Incrementing it invalidates existing refresh credentials for the user, while existing access credentials remain valid until expiry. This is a deliberate scope: group invalidation does not replace each refresh token on use or immediately revoke an access token. Durable counters and short access lifetimes are part of the deployment contract. ## Handling overlapping browser operations The browser client coordinates wallet sign-in, cached access credentials, and refresh requests. Access credentials live in memory unless storage is explicitly configured; the refresh credential remains in an HttpOnly cookie. Multiple callers share an in-flight refresh, and a failed request releases it so a later attempt can retry. Logout clears local state immediately and advances the session generation. A late response from the previous generation cannot restore authentication. The distinction between local state and server state still matters: if the logout HTTP request fails, the server cookie may remain valid and the application needs to handle that failure. Deferred credential commits are also available when an application must coordinate authentication with other state updates. - [Browser client reference](https://github.com/bridge-applications/world-auth-public#browser-client) ## Reusing the flow in production Pebbler World uses the package for wallet sign-in and authenticated sessions. Bridge uses it to protect file-transfer operations. Brain Bus, UnScrewed!, Picture Jam, Match & Win, and Color Jam share the browser integration through the internal htcc-kit package and use WorldAuthServer in their shared backend. The public repository checks real encrypted tokens, signed challenges, a signed SIWE proof, asynchronous session races, packed consumers, browser bundle isolation, and a Workers runtime smoke test. Native World App interactions, deployed cookie behavior, Redis durability, and contract-wallet RPC availability need checks in the consuming application’s environment. Keeping those boundaries explicit is part of making the package reusable. - [Explore the public repository](https://github.com/bridge-applications/world-auth-public) - [HTTP integration responsibilities](https://github.com/bridge-applications/world-auth-public/blob/main/docs/http-integration.md) - [Pebbler on World case study](https://tesselpunt.com/projects/pebbler-world) - [Bridge case study](https://tesselpunt.com/projects/bridge) - [Telegram authentication case study](https://tesselpunt.com/projects/tg-auth) --- # provider-kit - [Case study](https://tesselpunt.com/projects/provider-kit) How I built a reusable React state toolkit with schema-inferred types, runtime validation, and one provider for independent state slices. Structured, type-safe state for React. Role: Library design, implementation & maintenance Technologies: React, TypeScript, Zod - [Public repository](https://github.com/bridge-applications/provider-kit-public) I built provider-kit because I wanted a consistent way to share state across React components as my applications grew. State needed to stay strictly typed, updates needed a clear structure, and adding another piece of shared state shouldn’t mean adding another nested provider to main.tsx. The library brings those decisions into a reusable package, used across applications including Pebbler, Bridge, and Asset One. Its source is now public on GitHub, together with the tests and documentation that explain its behavior. ## Used by - [Pebbler® for Telegram](https://tesselpunt.com/projects/pebbler-telegram) - [Pebbler® on World](https://tesselpunt.com/projects/pebbler-world) - [Bridge](https://tesselpunt.com/projects/bridge) - [Asset One](https://tesselpunt.com/projects/asset-one) - Brain Bus - UnScrewed! - Picture Jam - Match & Win - Color Jam ## Package example Inside SlicesProvider · typed counter slice with schema-validated updates ```tsx const { state, setSlice } = useSlice('counter'); const increment = () => setSlice((previous) => ({ count: previous.count + 1, })); return ( ); ``` ## Keeping shared state organised React context provides a useful foundation for shared state. As an application gains more independent pieces of state, though, each context can bring its own provider, hook, types, and update conventions. I wanted a repeatable way to define those pieces without making the application’s entry point responsible for wiring them all together. My approach was to group state into named slices and let a factory create the contexts, provider, and hook. Each slice keeps its own context internally. The application mounts one provider component, while the library handles the composition behind it. ## Defining state once I use Zod schemas to describe each slice. TypeScript infers the state shape from that definition, and the same schema validates initial values and changed state at runtime. This keeps the runtime rules and the types components use connected, rather than maintaining two separate descriptions of the same data. In this example, filters and selection are independent slices. The factory returns a SlicesProvider and a useSlice hook bound to that configuration. The hook knows that filters has a string query and a numeric page; an unknown slice name or an incorrectly typed patch is a compile-time error. ### Two slices, one provider A complete, simplified example using the public API. The factory is created outside render; the filters schema supplies defaults and validates updates. ```tsx import { createSlicesProvider } from '@bridge-applications/provider-kit'; import { z } from 'zod'; const { SlicesProvider, useSlice } = createSlicesProvider({ filters: { schema: z.object({ query: z.string().default(''), page: z.number().int().positive().default(1), }), initial: {}, }, selection: { schema: z.object({ ids: z.array(z.string()) }), initial: { ids: [] }, }, }); function Search() { const { state, setSlice } = useSlice('filters'); return ( setSlice({ query: event.target.value, page: 1 }) } /> ); } export function App() { return ; } ``` - [Public API and usage reference](https://github.com/bridge-applications/provider-kit-public#usage) ## Making ownership and lifetime explicit The factory creates stable contexts once, outside render. Ordinary parent renders then preserve the mounted provider’s slices and child state. This is an important detail: recreating the factory during render would create a different provider component and reset that subtree. Each provider mount owns independent state. Separate factories are isolated even when they use the same slice names, so a hook cannot accidentally read another factory’s data. This makes ownership local to the part of the application that mounts the provider. When a reset is intentional, such as switching accounts, changing the provider’s React key starts a fresh subtree. ## Keeping updates predictable Updates use partial object patches with a shallow merge. Changing the search query can also reset the page in one update, as the example shows. Nested objects and arrays are replaced as complete fields, which keeps the update rule explicit. When the next value depends on previous state, a functional patch reads the latest queued state, including earlier updates in the same batch. The schema checks the resulting changed state before it becomes the next value. Schemas and updater functions must be pure and synchronous, because React can invoke them more than once during development checks. Defaults and refinements fit this model; asynchronous work belongs outside the schema. Validation failures have a deliberate boundary. Invalid initial state fails during factory creation; an invalid update reaches a React error boundary during update processing. Expected input errors, such as a user entering an invalid form value, should be handled before committing state. The library’s validation protects the state contract rather than supplying form feedback. ## Choosing a focused scope Separate contexts limit update notifications to the changed slice. Every consumer of that slice still receives the update, even if it only reads one field, and parent renders can still render their children. Empty or shallowly unchanged patches preserve state identity. These choices suit shared UI state while keeping the rendering model close to React’s own behavior. I kept the package focused on application state: filters, selections, upload queues, and similar concerns. It does not provide request caching, persistence, field selectors, or transactions across slices. Those requirements deserve their own solutions. Keeping the boundary clear makes the package easier to reason about and lets each application choose the other tools it needs. ## From application code to a public package Reusing provider-kit across products made the behavior of the shared layer worth documenting and checking independently. The public source release includes tests for validation, queued updates, provider isolation, rendering behavior, and server rendering. Compile-time checks exercise both accepted and rejected types. The repository also checks the packed package in a separate consumer, covering ESM imports, TypeScript declaration resolution, and browser bundling. That verifies what an application actually imports, alongside the implementation tests. React and Zod remain peer dependencies, and the library supplies no global mutable state store. I’m sharing the source so other developers can inspect the design, try the examples, and contribute improvements through the repository. The README explains the API and its constraints, while the architecture notes go deeper into state ownership, updates, and validation. The aim remains the one I started with: a clean way to grow shared state while keeping its types and behavior explicit. - [Explore the public repository](https://github.com/bridge-applications/provider-kit-public) - [Architecture notes](https://github.com/bridge-applications/provider-kit-public/blob/main/docs/architecture.md) - [Contributing](https://github.com/bridge-applications/provider-kit-public/blob/main/CONTRIBUTING.md)