<?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: Andrii Shupta</title>
    <description>The latest articles on DEV Community by Andrii Shupta (@andriishupta).</description>
    <link>https://dev.to/andriishupta</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%2F687483%2Fbc1fe17b-d22a-4196-9c6c-0316ccab8f87.png</url>
      <title>DEV Community: Andrii Shupta</title>
      <link>https://dev.to/andriishupta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andriishupta"/>
    <language>en</language>
    <item>
      <title>Encois: Building Organizational Intelligence on Google Cloud</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Thu, 24 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/encois-building-organizational-intelligence-on-google-cloud-6l3</link>
      <guid>https://dev.to/andriishupta/encois-building-organizational-intelligence-on-google-cloud-6l3</guid>
      <description>&lt;p&gt;I built Encois for the All Things Agentic Hackathon to explore how an AI system can follow changing company context without becoming an unrestricted chatbot. This article explains the system design behind its event-driven control plane, durable Temporal workflows, scoped agents and evidence model, including why PostgreSQL, raw artifacts, Organization Memory and Workflow Memory have separate responsibilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/encois" rel="noopener noreferrer"&gt;Encois repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allthingsagentichackathon.devpost.com/" rel="noopener noreferrer"&gt;All Things Agentic Hackathon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/encois/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;Encois architecture documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/encois/blob/main/docs/contracts.md" rel="noopener noreferrer"&gt;Encois contracts documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/encois/blob/main/docs/security.md" rel="noopener noreferrer"&gt;Encois security model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/encois/blob/main/docs/demo.md" rel="noopener noreferrer"&gt;Encois demo guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/" rel="noopener noreferrer"&gt;Temporal documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/adk-docs/" rel="noopener noreferrer"&gt;Google Agent Development Kit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Encois is a small organizational intelligence system. The easiest way to describe the idea is a mini-Palantir for companies: it connects information from different parts of a business and helps people understand what is changing. Its read-oriented product loop is deliberately narrow: &lt;strong&gt;observe → correlate → explain → recommend&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A company with 50–100 people continuously produces new context. A Jira issue becomes blocked, a pull request fails its checks, a deployment goes wrong, a customer sends an important email or an internal decision changes a release plan.&lt;/p&gt;

&lt;p&gt;Each event means little in isolation. The useful context appears when a failed check belongs to a pull request, the pull request blocks a Jira issue, the issue belongs to Friday's release and the release affects a customer commitment.&lt;/p&gt;

&lt;p&gt;The question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What changed, why does it matter, and what needs attention now?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why this is more than a RAG chatbot
&lt;/h2&gt;

&lt;p&gt;The obvious implementation would collect company documents, put them into a vector database and add chat. Retrieval can help find relevant semantic context, but a company is a changing system rather than a static collection of documents.&lt;/p&gt;

&lt;p&gt;Encois is therefore designed around Integrations, scoped Sources, webhooks, immutable revisions and durable Workflows. Every useful result should remain connected to real events, authorized data, user permissions, workflow history and inspectable evidence.&lt;/p&gt;

&lt;p&gt;RAG may support retrieval, but it must not decide who can access data, whether a Workflow completed or whether an external action is allowed. Those decisions belong to deterministic application boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  A release-safety example
&lt;/h2&gt;

&lt;p&gt;Imagine a delivery manager asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Is this release still safe to ship on Friday?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer may depend on open Jira work, GitHub pull requests, failed checks, deployment events and a document containing the release requirements. An Encois Workflow can collect those signals and return three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observed facts&lt;/strong&gt; — what Jira, GitHub and other Sources returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interpretation&lt;/strong&gt; — which blockers or dependencies were found across those facts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendation&lt;/strong&gt; — what a person should review or decide next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The recommendation does not hide its evidence. The user can inspect the issue, failed check, source and observation time behind it. That product requirement drives the need for provenance, freshness, identity, scope and durable execution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5dour6enu1kmoc5dm55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5dour6enu1kmoc5dm55.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with system design
&lt;/h2&gt;

&lt;p&gt;Encois was a hackathon project built around Google Cloud and enterprise agents. Before building most of the application, I defined where state should live, how events should enter the system, how permissions should survive a long-running Workflow and how agents should access tools. The goal was one demonstrable vertical slice without collapsing the control plane, execution engine, agent runtime and data plane into one service.&lt;/p&gt;

&lt;p&gt;The submission also needed to demonstrate autonomous behavior beyond a chat loop. I used Source ingestion and a multi-step release investigation as that vertical slice: an event enters through a typed boundary, Temporal keeps the work durable, bounded agents collect and interpret evidence, and the Dashboard presents a reviewable result.&lt;/p&gt;

&lt;p&gt;At article scale, the architecture is easier to read as two connected views:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj99anqxcyeske45b0d62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj99anqxcyeske45b0d62.png" alt=" " width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/andriishupta/encois/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;architecture documentation&lt;/a&gt; contains the complete deployment view with service identities, secrets, telemetry and local adapters.&lt;/p&gt;

&lt;p&gt;The important decision was giving each part one clear responsibility rather than adding services for the sake of the diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product concepts and immutable execution
&lt;/h2&gt;

&lt;p&gt;Encois separates the product into a few explicit concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integration&lt;/strong&gt; — an organization-level connection to GitHub, Jira or another provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt; — a repository, project, document or other scoped provider resource.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Revision&lt;/strong&gt; — an immutable version of Source data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template&lt;/strong&gt; — a reviewed starting pattern for a Workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blueprint&lt;/strong&gt; — the resolved and approved definition of its steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt; — a named process the organization can run again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run&lt;/strong&gt; — one durable execution of a Workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; — a reference supporting a fact or conclusion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxx2p516avenx6ltxlklg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxx2p516avenx6ltxlklg.png" alt=" " width="800" height="175"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This structure makes execution inspectable. If a Template changes tomorrow, an old Run still points to the approved Blueprint used at the time instead of silently reinterpreting history through new configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiqqffjvpxwq19pknj76.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiqqffjvpxwq19pknj76.png" alt=" " width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Events, Sources and changing context
&lt;/h2&gt;

&lt;p&gt;An organizational intelligence system cannot depend only on manual questions. Integrations connect Encois to company systems, while Sources narrow those connections to resources a team is allowed to use. Webhooks and ingestion Workflows bring new events and revisions into the platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37470igpwcjt1i1y59y5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37470igpwcjt1i1y59y5.png" alt=" " width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Provider credentials remain references resolved through Secret Manager. Large raw artifacts belong in Cloud Storage, while PostgreSQL stores the Source, its authorized scope, immutable revision and artifact reference. Temporal receives references and execution context rather than raw documents or credentials.&lt;/p&gt;

&lt;p&gt;The private Agent Gateway reads the artifact, normalizes it and extracts facts with provenance. Evidence preserves the provider record, observed and ingestion times, freshness, transformation version and visibility scope where available. Hosted adapters can project structured facts into Spanner Graph and distill relevant semantic context into Memory Bank. Derived memory remains useful context, but it is never stronger evidence than the event or document it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporal as the execution source of truth
&lt;/h2&gt;

&lt;p&gt;Agent work often lasts longer than one HTTP request. It can include several tools, temporary failures, retries, external events, human approval or a pause while waiting for more information.&lt;/p&gt;

&lt;p&gt;Temporal owns the durable execution state: Workflow history, retries, timers, Signals, cancellation, recovery and state between steps. PostgreSQL stores the product-facing Run projection, evidence, actor, scope and audit data without trying to reproduce Temporal's state machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Coordinator per organization
&lt;/h3&gt;

&lt;p&gt;Each organization has one long-lived Coordinator Workflow. It receives versioned lifecycle events such as an Integration being connected, a Source becoming ready, a Workflow start being requested or a Run completing. Temporal Signals and Continue-As-New let it preserve coordination without growing one unbounded Workflow history.&lt;/p&gt;

&lt;p&gt;The Gateway writes product state and an outbox event in the same PostgreSQL transaction. A dispatcher leases the row and delivers the versioned event to the Coordinator with retries. Delivery is at least once, so duplicate events are expected and deduplicated by stable event and Workflow identities. This avoids saving a Run while losing the request before Temporal receives it.&lt;/p&gt;

&lt;p&gt;For the hackathon version, the dispatcher stays inside the Gateway API. It can later move behind Pub/Sub or Kafka without changing the event contract or creating a second workflow engine.&lt;/p&gt;

&lt;p&gt;This also defines the consistency model shown to the user. A queued PostgreSQL Run appears as &lt;code&gt;preparing&lt;/code&gt; before Temporal exposes the execution. After the start, Temporal owns execution status and history, while PostgreSQL stores the query-friendly projection, evidence, actor, scope and audit metadata. If either side is unavailable, the API reports stale or unavailable state instead of manufacturing agreement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pdwyhv10yqef9ien5hq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pdwyhv10yqef9ien5hq.png" alt=" " width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Generic Blueprint execution
&lt;/h3&gt;

&lt;p&gt;The Go Runtime executes approved Blueprints through a generic Temporal Workflow. A Blueprint can contain tool steps, agent steps, deterministic transforms, dependencies, conditions, timers and approval Signals. Independent ready steps can run in parallel.&lt;/p&gt;

&lt;p&gt;The current implementation runs one approved Google ADK Agent Definition as one Temporal Activity. Temporal can retry that bounded unit while model and tool calls remain outside deterministic Workflow code. This gives internal agent turns coarser visibility and retry behavior, a trade-off that a deeper Temporal and ADK integration could address later. A &lt;code&gt;waiting&lt;/code&gt; Run remains durable while it waits for a timer, external condition or approval Signal; &lt;code&gt;paused&lt;/code&gt; is a separate versioned product command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffl4t5nfydto98p795y8c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffl4t5nfydto98p795y8c.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bounded agents and deterministic boundaries
&lt;/h2&gt;

&lt;p&gt;Encois does not give one agent every tool and ask it to operate the organization. Each agent has a narrow role, typed input and output, an allowlist of tools, a timeout, retry limit, budget and organization scope.&lt;/p&gt;

&lt;p&gt;Authentication, permissions, scope calculation, Workflow transitions, schema validation, deduplication and approval rules remain deterministic code. Google ADK and Gemini handle bounded reasoning and synthesis. The model can interpret evidence, but it cannot decide that a user belongs to another organization or that a failed Run succeeded.&lt;/p&gt;

&lt;p&gt;The private Agent Gateway checks service identity, a short-lived signed execution capability, organization scope, actor, policy version and the requested tool. Provider credentials never enter the browser, model or Temporal history. Tool names, arguments and provider content remain untrusted until they pass registry, schema and policy checks. Tools are read-only by default; any future write to Jira, GitHub or another provider needs separate permission, approval, audit, idempotency and recovery boundaries.&lt;/p&gt;

&lt;p&gt;A recommendation is not an automatic action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contracts are part of the architecture
&lt;/h3&gt;

&lt;p&gt;The public Gateway is TypeScript, while the Runtime and Agent Gateway are Go. Their boundaries use versioned OpenAPI and JSON Schema contracts rather than shared source files or database models. Coordinator events, Workflow inputs, tool manifests, evidence and private service payloads are validated on both sides; unknown versions and invalid states fail closed.&lt;/p&gt;

&lt;p&gt;The Runtime never queries the control-plane database to reconstruct missing application state. An approved Blueprint snapshot and execution context enter through Temporal, while provider-specific payloads stop at the Agent Gateway and become small typed evidence records. This keeps language and provider choices behind explicit adapters instead of turning PostgreSQL into an integration bus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clear ownership across Google Cloud
&lt;/h2&gt;

&lt;p&gt;Each Google Cloud service has a specific responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime — Cloud Run.&lt;/strong&gt; Hosts the Dashboard, Gateway API, Agent Runtime and private Agent Gateway as independently deployable services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control and artifacts — Cloud SQL for PostgreSQL and Cloud Storage.&lt;/strong&gt; PostgreSQL owns product state, permissions, Runs, outbox rows and projections; Storage owns large raw Source artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organizational context — Spanner Graph.&lt;/strong&gt; Stores structured company facts and relationships with provenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent context — Agent Platform Memory Bank.&lt;/strong&gt; Stores agent-specific semantic context between Workflows without becoming the source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning — Vertex AI and Gemini.&lt;/strong&gt; Receives validated, scoped inputs for reasoning and synthesis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust and operations — Secret Manager, Cloud Logging and Cloud Trace.&lt;/strong&gt; Protects provider credentials and records timing, retries, evidence reads and error classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source-of-truth split is easier to see as state ownership:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Source of truth&lt;/th&gt;
&lt;th&gt;Consumer-facing role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity, permissions, configuration, Runs and outbox&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Gateway authorization and Dashboard projections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow history, retries, waits and cancellation&lt;/td&gt;
&lt;td&gt;Temporal&lt;/td&gt;
&lt;td&gt;Durable execution and live Run status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original Source payloads and large artifacts&lt;/td&gt;
&lt;td&gt;Cloud Storage&lt;/td&gt;
&lt;td&gt;Scoped reads through the Agent Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured facts and relationships&lt;/td&gt;
&lt;td&gt;Spanner Graph&lt;/td&gt;
&lt;td&gt;Evidence-linked organizational queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent-specific semantic context&lt;/td&gt;
&lt;td&gt;Memory Bank&lt;/td&gt;
&lt;td&gt;Scoped retrieval across related Workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The separation between PostgreSQL, Spanner Graph and Memory Bank is deliberate. PostgreSQL is the control plane, Spanner Graph models durable organizational facts and relationships, and Memory Bank supports agent retrieval. None of them replaces Temporal's execution history or the original Source artifact.&lt;/p&gt;

&lt;p&gt;The current Graph integration projects scoped fact nodes with provenance. Rich entity normalization and relationship edges remain future work, which is why the article treats them as a direction rather than a finished enterprise knowledge graph.&lt;/p&gt;

&lt;p&gt;Go was outside my main stack, but it was a practical fit for the Temporal worker and Google ADK runtime. AI helped me build the first small implementation, although learning Go properly still requires more time with the language and SDK documentation.&lt;/p&gt;

&lt;p&gt;Observability follows the same boundaries. Request and trace IDs, organization scope, Workflow identity and Run identity travel across the Gateway, outbox, Temporal Activities and private tool requests. Structured telemetry records duration, retry count, provider, model and error class without storing raw credentials, unrestricted documents or hidden model reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions must survive the Workflow
&lt;/h2&gt;

&lt;p&gt;Organization is the hard tenant boundary in Encois. Inside it, access narrows through organizational units:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feheglyje5hytzmsl40cl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feheglyje5hytzmsl40cl.png" alt=" " width="800" height="724"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Gateway calculates effective scope from the authenticated membership and selected unit. Checking permission only during the first HTTP request would be insufficient because a Workflow can continue long after that request ends.&lt;/p&gt;

&lt;p&gt;The same organization, actor, authorized unit IDs and capability travel through the outbox, Temporal Workflow and private tool requests. The Runtime and Agent Gateway verify them again at their own boundaries. A model-generated organization ID never becomes authorization simply because it appeared in a payload.&lt;/p&gt;

&lt;p&gt;The system also fails closed. Missing onboarding data is an error, an unknown Run status is a contract mismatch and provider authentication failure remains visible as degraded or requiring reauthorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the dashboard into an operational view
&lt;/h2&gt;

&lt;p&gt;The first Dashboard exposed Integrations, Sources, Templates, Blueprints, Workflows, Runs, events, permissions and memory. That was useful for validating the backend and demonstrating that the control plane was real, but it also made the product feel like an interface to the database.&lt;/p&gt;

&lt;p&gt;After testing that version, I shifted the UI toward one operational view that answers a smaller set of questions: what changed, what needs attention, which release is blocked, what has already been checked and which conclusion still needs a human decision.&lt;/p&gt;

&lt;p&gt;The underlying control-plane screens remain necessary for configuration and debugging. They should support the intelligence experience rather than become its main interface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxc8rq5w4ulf20fhwv7o5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxc8rq5w4ulf20fhwv7o5.png" alt=" " width="799" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Current limits and next steps
&lt;/h2&gt;

&lt;p&gt;The local environment keeps the same application topology with Docker Compose, PostgreSQL, Temporal, Auth and Storage emulators, the Dashboard, Gateway, Runtime and Agent Gateway. It seeds a synthetic company called &lt;code&gt;Sun Inc&lt;/code&gt; with real control-plane records and Temporal-backed Runs; provider fixtures and infrastructure mocks remain explicit adapter modes.&lt;/p&gt;

&lt;p&gt;Real Gemini, Spanner Graph and Memory Bank integrations are opt-in because they require configured Google Cloud resources and may incur cost.&lt;/p&gt;

&lt;p&gt;The hackathon MVP still needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;richer Graph entities and relationships;&lt;/li&gt;
&lt;li&gt;production validation of hosted IAM, retention and cost;&lt;/li&gt;
&lt;li&gt;full organization-unit isolation in the hosted Memory Bank adapter;&lt;/li&gt;
&lt;li&gt;hardened provider authorization and lifecycle behavior;&lt;/li&gt;
&lt;li&gt;independent outbox delivery at larger scale;&lt;/li&gt;
&lt;li&gt;more granular Temporal and ADK execution;&lt;/li&gt;
&lt;li&gt;stronger deployment and disaster-recovery processes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next product step is a smaller and more useful operational picture. Scheduled briefings and change detection could surface investigations before a user asks, while conversational or voice interfaces could help explore evidence without replacing identity, scope and audit boundaries.&lt;/p&gt;

&lt;p&gt;Agents may later propose Sources, Blueprints or Workflow configurations, but those proposals should remain reviewable. External actions need stronger approval and recovery guarantees than recommendations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Encois explores organizational AI as an event-driven system rather than a chatbot over documents. Its design separates product state, durable execution, raw evidence, structured organizational facts, agent memory and model reasoning so each layer has one clear source of truth.&lt;/p&gt;

&lt;p&gt;Temporal makes long-running agent work recoverable. Google Cloud provides explicit boundaries for services, artifacts, secrets, organizational context and model execution. Signed scope and capabilities preserve authorization after the original browser request has ended, while evidence keeps facts, interpretations and recommendations distinguishable.&lt;/p&gt;

&lt;p&gt;The main product direction is equally clear: users need one operational view of what changed and what deserves attention, with the architecture underneath making every result scoped, durable and inspectable.&lt;/p&gt;

</description>
      <category>encois</category>
      <category>ai</category>
      <category>googlecloud</category>
      <category>temporal</category>
    </item>
    <item>
      <title>MiyKo: A Durable AI Workflow for Shared Shopping</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/miyko-a-durable-ai-workflow-for-shared-shopping-1bnm</link>
      <guid>https://dev.to/andriishupta/miyko-a-durable-ai-workflow-for-shared-shopping-1bnm</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ai-factory.silpo.ua/" rel="noopener noreferrer"&gt;AI Factory by Silpo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/miyko" rel="noopener noreferrer"&gt;MiyKo GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.langchain.com/oss/javascript/langgraph/overview" rel="noopener noreferrer"&gt;LangGraph documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.mem0.ai/introduction" rel="noopener noreferrer"&gt;Mem0 documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built MiyKo for the AI Factory by Silpo hackathon to test a harder version of AI shopping: one order shared by several people over time. The result connects a React Native app, a role-aware API, a durable LangGraph workflow, Mem0 memory and Silpo’s real basket. This article explains the system boundaries that made the shared order work, where human approval stays in control, and what would need to change before production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Silpo already has an AI assistant that can find products and help with a basket. I wanted to see what changes when the unit of work is a &lt;strong&gt;shared order&lt;/strong&gt; , rather than one prompt from one person.&lt;/p&gt;

&lt;p&gt;A shared order can outlive the first chat: one person starts dinner planning, another adds drinks, a child requests ice cream, an owner approves it, and the basket is prepared hours later. That makes identity, permissions, durable state and current provider data first-class parts of the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a shared order changes the product
&lt;/h2&gt;

&lt;p&gt;A group can be a family, flatmates, coworkers planning lunch or friends organizing a picnic. The same flow also works for one person.&lt;/p&gt;

&lt;p&gt;Imagine a simple dinner:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mom starts the order.&lt;/li&gt;
&lt;li&gt;Dad adds drinks.&lt;/li&gt;
&lt;li&gt;A child asks for ice cream.&lt;/li&gt;
&lt;li&gt;The child’s request waits for approval.&lt;/li&gt;
&lt;li&gt;The family confirms the plan.&lt;/li&gt;
&lt;li&gt;MiyKo prepares the real store basket.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everyone works with the same workflow, but not everyone has the same permissions.&lt;/p&gt;

&lt;p&gt;In the MVP, an owner or admin can approve requests and perform provider actions. An editor can add to the shared plan. A viewer can make a request, but it must be approved before it affects the real basket.&lt;/p&gt;

&lt;p&gt;This is important because the AI does not decide who has authority. The API knows the authenticated member and their role. A prompt cannot turn a viewer into an owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the architecture small
&lt;/h2&gt;

&lt;p&gt;Silpo was the real provider used for the hackathon. The provider boundary keeps the shared-order design independent of that particular store.&lt;/p&gt;

&lt;p&gt;MiyKo has five main runtime boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo / React Native&lt;/strong&gt; for the mobile experience;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hono API&lt;/strong&gt; for authentication, group roles and provider connections;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangGraph&lt;/strong&gt; for the active workflow and pause/resume state;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mem0&lt;/strong&gt; for long-term group and member memory;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silpo MCP&lt;/strong&gt; for current products, basket operations, fulfillment and checkout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete request path looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0qe4vbe4pc58patwaue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0qe4vbe4pc58patwaue.png" alt=" " width="800" height="1292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The API is the security boundary. LangGraph owns the order state and pause/resume transitions. LangChain provides the model and tool integration inside that graph; Mem0 supplies long-term context when needed. The model proposes a next step, while the API checks the authenticated actor and permissions before protected actions. When that step needs store data or a basket change, it calls Silpo through MCP. The result returns to LangGraph, is saved in the workflow state and becomes the next status shown in the app.&lt;/p&gt;

&lt;p&gt;PostgreSQL is a small control plane and records the outbox events that bridge API commands to the workflow. It stores users, groups, roles, provider bindings, workflow references, approval decisions, audit records and outbox state. Each order has one workflow UUID, reused as its LangGraph thread ID, so later actions resume the same process. The outbox worker retries temporary delivery failures with an idempotency key; an owner’s decision to decline a request remains final.&lt;/p&gt;

&lt;p&gt;It does not store a second product catalog, a second basket or a copy of LangGraph checkpoints. This keeps a single source of truth for each kind of data.&lt;/p&gt;

&lt;p&gt;This was an important decision. Creating tables for everything would make the system look complete, but it would also create difficult questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which price is correct when Silpo changes it?&lt;/li&gt;
&lt;li&gt;What happens when a locally stored product becomes unavailable?&lt;/li&gt;
&lt;li&gt;Which copy of the basket is the source of truth?&lt;/li&gt;
&lt;li&gt;How do two workflow states stay synchronized?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simpler answer is to leave provider data with the provider, workflow state with LangGraph and reusable memory with Mem0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete MiyKo flow
&lt;/h2&gt;

&lt;p&gt;The demo follows one shared order from the first request to the Silpo checkout.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. One workflow keeps the process together
&lt;/h3&gt;

&lt;p&gt;The MVP uses one workflow kind called &lt;code&gt;step-order&lt;/code&gt;. LangGraph keeps the active state, pauses when a decision is needed and resumes the same workflow later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqi9ex9x7nd0ralp201q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqi9ex9x7nd0ralp201q.png" alt="MiyKo step-order workflow graph" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This graph is not only a chain of AI calls. It represents the order lifecycle: planning, member changes, approvals, provider actions and completion. The screenshot shows the graph in Studio. LangGraph runs the workflow; LangSmith provides development and tracing tools around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Different people see the same order
&lt;/h3&gt;

&lt;p&gt;Each group member has a separate authenticated session. They do not share login credentials, but they can see the same active workflow.&lt;/p&gt;

&lt;p&gt;The group view shows members, roles and the connected provider. The owner connects Silpo once for the group through OAuth 2.1 with PKCE; MiyKo never asks for the store password.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0n7mlwsmo3390x41zfz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0n7mlwsmo3390x41zfz.png" alt=" " width="790" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Other members can use that connection through the workflow, but they never receive the provider token.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Silpo stays a separate trusted system
&lt;/h3&gt;

&lt;p&gt;The credentials remain encrypted on the backend. When the workflow calls Silpo MCP, the API supplies the access token as runtime-only context. The mobile app, LangGraph state and Mem0 do not hold that token.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Previous purchases become useful memory
&lt;/h3&gt;

&lt;p&gt;Before starting the first order, the owner can import a summary of recent Silpo receipts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7h6gxr1u8u9f2zblensa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7h6gxr1u8u9f2zblensa.png" alt="Recent Silpo purchases stored as MiyKo memory" width="799" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This gives MiyKo context such as products the group bought before or choices that may be useful again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5p67z7tdvq32clyokknv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5p67z7tdvq32clyokknv.png" alt="MiyKo group memory in Mem0" width="800" height="705"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Memory helps the agent make a better suggestion. It does not give permissions and it does not replace the current provider data.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The plan exists before the real basket
&lt;/h3&gt;

&lt;p&gt;The owner starts a dinner workflow in the React Native app. MiyKo reads relevant memory and creates the initial shared plan. At this point, the Silpo basket is still untouched. The screens below show the progression from an initial plan to a prepared basket; the provider action happens only after the group confirms the plan.&lt;/p&gt;

&lt;p&gt;I wanted planning and execution to be separate. During planning, people can speak naturally, add ideas and change their minds. Before execution, the system must check the actor, permissions and current provider state.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The workflow pauses for approval
&lt;/h3&gt;

&lt;p&gt;When a viewer asks to add ice cream, the request remains in the same workflow and waits for an owner or admin.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqggk15oshyoblrdaaf7k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqggk15oshyoblrdaaf7k.png" alt="Group request and owner approval in MiyKo" width="800" height="858"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After approval, LangGraph resumes the same thread. The context is not copied into a new chat and the order is not recreated from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. The agent prepares a real basket
&lt;/h3&gt;

&lt;p&gt;Only after the group agrees on the plan does an authorized member ask MiyKo to prepare the basket.&lt;/p&gt;

&lt;p&gt;The workflow discovers the current Silpo MCP tools, resolves products and updates the real provider basket. Product names, images, prices and totals come from Silpo, not from a local MiyKo catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcek9t8t9nwa8ve1ii6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcek9t8t9nwa8ve1ii6w.png" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Checkout still belongs to Silpo
&lt;/h3&gt;

&lt;p&gt;Silpo MCP can work with the basket and return a checkout link, but it does not expose a final place-order tool. MiyKo prepares the basket; the owner finishes the purchase in Silpo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh7f1gq4lpote7582bkcb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh7f1gq4lpote7582bkcb.png" alt=" " width="800" height="1542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That boundary is intentional. MiyKo must not say that an order is complete when it only has a basket and a checkout URL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0mj3x1defcjewjcvnbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0mj3x1defcjewjcvnbk.png" alt="Completed MiyKo workflow on two group dashboards" width="800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot shows a completed MiyKo workflow. The final provider confirmation still happens in Silpo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory, not chat history
&lt;/h2&gt;

&lt;p&gt;I was not interested in storing a long chat transcript and calling it memory.&lt;/p&gt;

&lt;p&gt;I wanted memory to be managed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add useful memories;&lt;/li&gt;
&lt;li&gt;update them when preferences change;&lt;/li&gt;
&lt;li&gt;remove them when they are wrong or no longer relevant;&lt;/li&gt;
&lt;li&gt;keep shared group memory;&lt;/li&gt;
&lt;li&gt;keep personal member context;&lt;/li&gt;
&lt;li&gt;keep order-specific state inside the active workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are different things.&lt;/p&gt;

&lt;p&gt;Mem0 stores long-term group and member memory. LangGraph keeps the transient context for the current order. PostgreSQL does not duplicate either one.&lt;/p&gt;

&lt;p&gt;For example, “the group often buys food for two days” can be long-term memory. “Add ice cream to tonight’s order” belongs to the active workflow. The current ice cream price still belongs to Silpo.&lt;/p&gt;

&lt;p&gt;This separation also protects the permission model. Memory may say that a child likes ice cream, but it cannot approve the purchase. The API still checks the real member role.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reworking the app UI
&lt;/h2&gt;

&lt;p&gt;The first version of the React Native UI came from a simpler model. It worked, but the screens were confusing. I opened the app, walked through the order flow, and tested where the experience broke down before redesigning it.&lt;/p&gt;

&lt;p&gt;That gave me specific problems to fix: which person is acting, which request is waiting for approval, whether the basket is still a plan or already prepared, and what the next action should be. Once the flow made sense, I rebuilt the screens around those states. Seeing and testing the real interface was more useful than asking a model for a polished screen in one prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;MiyKo’s design starts with a shared order that can continue across people and time. The API verifies identity and permissions; LangGraph keeps the order resumable; Mem0 supplies reusable context; and the provider remains the source of truth for products, prices and checkout. Keeping these responsibilities separate prevents a remembered preference or a model suggestion from becoming an unauthorized action.&lt;/p&gt;

&lt;p&gt;The same separation shaped the app: people need to see whether they are planning, waiting for approval or acting on a real basket. Testing the first interface exposed those gaps before the redesign. Silpo supplied the concrete integration for this project, but the useful result is the architecture and interaction model for coordinating a group around an external action.&lt;/p&gt;

</description>
      <category>miyko</category>
      <category>ai</category>
      <category>langgraph</category>
      <category>mem0</category>
    </item>
    <item>
      <title>I Built a Fasting App I'd Actually Use. Apple Rejected It.</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/i-built-a-fasting-app-id-actually-use-apple-rejected-it-39i6</link>
      <guid>https://dev.to/andriishupta/i-built-a-fasting-app-id-actually-use-apple-rejected-it-39i6</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/simple-fasting" rel="noopener noreferrer"&gt;Simple Fasting source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simplefasting.app/" rel="noopener noreferrer"&gt;Simple Fasting website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/app-store/review/guidelines/" rel="noopener noreferrer"&gt;App Store Review Guidelines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple rejected Simple Fasting under Guideline 4.3(a), “Design – Spam.” I had spent roughly two to three weeks building and polishing the app, perhaps closer to a month in total. Then I waited about a month for the first review. I replied to explain the app, but the rejection stood.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr80jml04bnqnwausyp9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr80jml04bnqnwausyp9d.png" alt="App Store Connect showing Simple Fasting for iOS 1.0.0 as rejected" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The message said the app shared a similar “binary, metadata, and/or concept” with apps from other developers, with only minor differences. It did not say which of those was decisive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foig1qiqgey4guvjvxznq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foig1qiqgey4guvjvxznq.png" alt="App Review message citing Guideline 4.3(a), Design – Spam, and similarity in binary, metadata, and/or concept" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I still use Simple Fasting. It is installed on my phone through TestFlight, and I am happy with the app I made. Of course, I would rather have it publicly available in the App Store. For now, I can use it myself even though the release was rejected.&lt;/p&gt;

&lt;p&gt;I built it myself with Expo and React Native; I did not repackage someone else's app. But I cannot pretend that a fasting tracker is a new concept. What felt different to me was the experience I wanted to use every day. That is where this story starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app I wanted to use
&lt;/h2&gt;

&lt;p&gt;I used fasting apps that began simply and grew into something more demanding. With Zero, what frustrated me most was opening the app to start or stop a fast and running into a paywall. Sometimes I had to wait for a close button to appear. At times I could not dismiss it and ended up closing the app entirely. I do not know whether that was a bug or an intended flow, but I knew I did not want that experience in my own tracker.&lt;/p&gt;

&lt;p&gt;My idea was small: open the app, start or stop a fast, and close it. No account, analytics, or subscription popup interrupting that action. Fasting history, goals, notes, and reminders would stay on the device unless the user chose to export them. Privacy was part of the product, not a feature to add later.&lt;/p&gt;

&lt;p&gt;Simple Fasting grew beyond a timer. It has local history and statistics, reusable goals, reminders, import and export, iOS and Android widgets, an optional iOS Live Activity, and a small website. But the main interaction still needed to feel calm and immediate.&lt;/p&gt;

&lt;p&gt;That constraint helped me decide what belonged in the first screen. The timer and its main action needed to be obvious. History and statistics could be there when I wanted them, without asking for attention every time I opened the app. I was willing to build more behind the scenes if it made the everyday interaction feel smaller.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxm9m3gaf3fmkuld4jqk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxm9m3gaf3fmkuld4jqk6.png" alt="Simple Fasting ready and active fasting screens in light and dark themes" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it with AI, without giving up the decisions
&lt;/h2&gt;

&lt;p&gt;I am a software engineer, and I used AI agents across product planning, architecture, React Native code, tests, UI iterations, documentation, and release work. That made implementation faster. It did not decide what the product should be or whether a screen felt right to use.&lt;/p&gt;

&lt;p&gt;Before building many features, I wrote down the product goal, privacy boundary, data behavior, and non-goals. This gave each new AI task the same starting point. If I asked for “a modern fasting app” without those boundaries, an agent might reasonably add authentication, cloud sync, or subscriptions. Those are common choices, but they were wrong for this app.&lt;/p&gt;

&lt;p&gt;I did not call it spec-driven development at first. I just wanted decisions to survive beyond one chat. An agent could complete a task well and still lose context before the next one. Written rules made it easier to catch a feature that worked technically but moved the product away from what I wanted.&lt;/p&gt;

&lt;p&gt;The repository contains the Expo app, a static Astro website, and shared help and legal content. The app's local history is the source of truth for statistics. Widgets and the Live Activity need to reflect the same active fast as the app. When several agents work on connected surfaces, clear ownership matters more than another long prompt.&lt;/p&gt;

&lt;p&gt;My usual loop was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;describe one outcome → implement it → run the app → use it → explain what feels wrong → adjust it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, “add an end-of-fast reminder” was not enough. The task needed to specify that the reminder is local, belongs to the active fast, changes when that fast changes, and never blocks the timer if notification permission is denied. AI could help build it once those product rules were clear.&lt;/p&gt;

&lt;p&gt;I also changed the amount of reasoning I used for different tasks. A narrow component or test did not need the same effort as a decision about storage or native behavior. What mattered was giving the task enough context and checking the result. A model's confident explanation was never evidence that the feature worked on a device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where faster coding made the app heavier
&lt;/h2&gt;

&lt;p&gt;AI makes another safety path feel cheap. I added retries, reset flows, and recovery behavior while the core product still needed real use. Some safeguards were justified: local data has no backend copy, and import and export give people control over it. Other paths created more states, warnings, tests, and maintenance than the first version needed.&lt;/p&gt;

&lt;p&gt;I learned to ask which real failure a feature handles before adding it. The cost of generating code is small; the cost of living with every extra state is not.&lt;/p&gt;

&lt;p&gt;This was especially tempting because the app kept expanding. A timer led to goals; goals led to reminders; sessions led to History and statistics. Each feature sounded modest on its own. Together they changed the number of ways the app could behave. If I were starting again, I would finish and use the central fasting flow longer before building so much recovery logic around it.&lt;/p&gt;

&lt;p&gt;I also delayed the design system. Early screens looked fine individually, but spacing, controls, and screen structure began to drift. I paused feature work for about two days to make shared components, theme tokens, and clearer visual rules. After that, I could ask an agent to reuse an existing section or button instead of describing its appearance from scratch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4qypm1rin5jz0pmqt5p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4qypm1rin5jz0pmqt5p.png" alt="Simple Fasting settings screens showing shared controls and theme behavior" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A design system gave the app consistency. It also made the AI work more precise. But it could not replace opening the product and seeing what happened on a real screen.&lt;/p&gt;

&lt;p&gt;The cleanup was not an attempt to make every screen identical. It gave repeated elements a common shape and let the differences serve the task. I still adjusted screens after seeing them in context, especially where native navigation and safe areas affected the layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screen had the final say
&lt;/h2&gt;

&lt;p&gt;Wireframes and specifications helped me choose a direction, but I made the final UI decisions by using the app. The iOS Simulator and Android emulator exposed content hidden by navigation, awkward scrolling, keyboard overlap, picker behavior, and differences between light and dark themes. A screenshot could show spacing; it could not show whether a selected goal survived reopening a tab.&lt;/p&gt;

&lt;p&gt;Specific feedback worked best. “This screen feels wrong” gave an agent little to act on. “The Start button is hidden behind the tab bar on a small iPhone” described a state, an action, and a visible failure.&lt;/p&gt;

&lt;p&gt;I did focused checks during iteration, then ran broader tests and builds once a feature was stable. Physical-device checks still mattered for notifications, widgets, and Live Activity. A successful build proved that the project built; it did not prove that the interaction was good.&lt;/p&gt;

&lt;p&gt;The most useful unit of work was a complete user flow. Choosing a goal, starting a fast, updating the widget, ending the session, and seeing it in History all depend on the same data. Building each screen in isolation could leave several incompatible versions of one fast.&lt;/p&gt;

&lt;p&gt;That is also why I did not treat a simulator screenshot as a release check. I could inspect the layout quickly there, while a native build and a physical device answered different questions. Notifications and widgets needed those later checks. The faster UI loop helped me make decisions sooner, but the broader checks still had to happen before submission.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtybml854wgh2pqy8tc5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtybml854wgh2pqy8tc5.png" alt="Simple Fasting statistics and history screens using the same session data" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;p&gt;The rejection did not tell me that the code was copied or that Expo was the problem. Apple's wording included binary, metadata, &lt;strong&gt;and/or&lt;/strong&gt; concept. I know how I built the app; I do not know which similarity mattered most to the reviewer.&lt;/p&gt;

&lt;p&gt;My best guess is that the concept mattered most: Simple Fasting is another tracker in a crowded category. I knew other fasting apps existed. I just thought that if I wanted a version without an obstructive paywall, I could build and submit my own. I did not expect an established idea itself to be a reason for rejection, especially when many similar apps are already in the store.&lt;/p&gt;

&lt;p&gt;Privacy, a clean UI, and no paywall in the core flow are meaningful differences for me. They may be too subtle to distinguish the app during review. I wonder whether clearer positioning, or a more visible feature of my own, would have helped. I cannot know that, or whether marketing or the developer's size played any role. Next time, I would find a distinctive product behavior early, even if the idea first sounds a little odd, and make its value obvious before spending weeks polishing the implementation.&lt;/p&gt;

&lt;p&gt;After the rejection, I made the previously private repository public so people could inspect the work behind Simple Fasting. I still like the app I built, and I would still use it. The experience taught me two things at once: AI is useful when I keep the product decisions and verify the result, and good engineering does not guarantee a place in the App Store.&lt;/p&gt;

&lt;p&gt;I have not turned this into a story about beating App Review. For now, the honest ending is that I made something useful to me, learned a great deal from building it, and could not publish it there. Showing the work publicly feels more useful than pretending the rejection did not happen.&lt;/p&gt;

</description>
      <category>simplefasting</category>
      <category>aiagents</category>
      <category>reactnative</category>
      <category>expo</category>
    </item>
    <item>
      <title>Directing a 3D Web Experience with AI</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/directing-a-3d-web-experience-with-ai-2p7o</link>
      <guid>https://dev.to/andriishupta/directing-a-3d-web-experience-with-ai-2p7o</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://astro-gsap-webflow.webflow.io/" rel="noopener noreferrer"&gt;Cathedral of Threads — live experience&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/astro-gsap-webflow" rel="noopener noreferrer"&gt;Source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/" rel="noopener noreferrer"&gt;Astro documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gsap.com/docs/" rel="noopener noreferrer"&gt;GSAP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://threejs.org/docs/" rel="noopener noreferrer"&gt;Three.js documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.webflow.com/webflow-cloud/bring-your-own-app" rel="noopener noreferrer"&gt;Webflow Cloud documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F557e90mqtezlep5u7laq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F557e90mqtezlep5u7laq.png" alt="Cathedral of Threads opening scene in its dark blue night theme" width="800" height="400"&gt;&lt;/a&gt; &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3ndhk9u7fsbyyt8ueq6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3ndhk9u7fsbyyt8ueq6.png" alt="Cathedral of Threads opening scene in its warm light theme" width="800" height="401"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The same opening scene in dark and light themes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I built &lt;em&gt;Cathedral of Threads&lt;/em&gt; for a small GSAP and Webflow challenge.&lt;/p&gt;

&lt;p&gt;The idea was to create a one-page 3D experience inspired by the Coordinate from Attack on Titan: a strange place outside normal time, with a huge tree, paths that feel like human memories, and a transition between night and day.&lt;/p&gt;

&lt;p&gt;The page is controlled by scroll. As you move down, the camera moves through the scene. The environment changes, the tree gets closer, and the story moves through several short chapters.&lt;/p&gt;

&lt;p&gt;I wanted it to feel more like a small digital artwork than a normal landing page.&lt;/p&gt;

&lt;p&gt;I used Astro for the website, GSAP for the scroll and animation, Three.js for the 3D scene, and Webflow mainly for deployment through its CLI. Most of the scene, animation, and interaction code was created through conversations with AI coding agents.&lt;/p&gt;

&lt;p&gt;The unusual part is that I did not know Three.js well when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  I started from the result, not the code
&lt;/h2&gt;

&lt;p&gt;I set up Astro, added GSAP, and prepared the project for Webflow. After that, I worked with the agent on the main experience.&lt;/p&gt;

&lt;p&gt;At first, I barely looked at the generated Three.js code.&lt;/p&gt;

&lt;p&gt;This may sound careless, but reading unfamiliar 3D code was not the best way for me to judge the result. I could see camera coordinates, curves, materials, lights, particles, and many numbers, but that did not tell me whether the scene felt right.&lt;/p&gt;

&lt;p&gt;So my main feedback loop was visual:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask the agent to make one change.&lt;/li&gt;
&lt;li&gt;Open the page.&lt;/li&gt;
&lt;li&gt;Scroll through the full scene.&lt;/li&gt;
&lt;li&gt;Decide what feels wrong.&lt;/li&gt;
&lt;li&gt;Explain the problem more clearly.&lt;/li&gt;
&lt;li&gt;Repeat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I added image references and described the composition I wanted. I explained where the tree should be, how large it should feel, how close the camera should move, which elements should stay in the background, and how the scene should change between night and day.&lt;/p&gt;

&lt;p&gt;The agents wrote most of the code. I directed the result.&lt;/p&gt;

&lt;p&gt;That sounds simple, but most of the work was hidden inside the last step: explaining exactly what “looks right” means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technically correct can still look wrong
&lt;/h2&gt;

&lt;p&gt;AI can generate a valid Three.js scene very quickly. It can create a renderer, camera, lights, materials, geometry, particles, and animation. The page can load without errors and still look bad.&lt;/p&gt;

&lt;p&gt;I saw this many times during the project.&lt;/p&gt;

&lt;p&gt;The tree existed, but it looked too small. The paths were animated, but they did not create depth. The camera moved, but it did not feel cinematic. The bloom worked, but it made the whole image flat. The transition between dark and light was technically there, but it did not feel like part of the story.&lt;/p&gt;

&lt;p&gt;These were not problems that a build command could find.&lt;/p&gt;

&lt;p&gt;I had to turn visual reactions into more useful instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Make it more cinematic” became “keep the camera low during the approach, then tilt upward near the tree.”&lt;/li&gt;
&lt;li&gt;“The scene feels empty” became “add more paths close to the camera, but keep the centre readable.”&lt;/li&gt;
&lt;li&gt;“The tree is weak” became “show its full silhouette in the first frame and make the branches denser near the top.”&lt;/li&gt;
&lt;li&gt;“The light mode does not work” became “change the world gradually during the journey instead of switching colours at one point.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more specific my feedback became, the better the agent became at changing the right part of the scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Three.js was difficult to review
&lt;/h2&gt;

&lt;p&gt;Normal UI code is easier for me to connect with the result. I understand what a button, grid, breakpoint, or CSS property should do. If something is wrong, I usually know where to look.&lt;/p&gt;

&lt;p&gt;Three.js was different.&lt;/p&gt;

&lt;p&gt;A visual result can depend on camera position, field of view, geometry, material, lighting, fog, post-processing, and the relationship between all of them. One small change can make the whole scene feel different.&lt;/p&gt;

&lt;p&gt;The maths also makes code review harder when you do not know the library. Looking at a group of vectors and curves does not immediately tell you what the camera journey will feel like.&lt;/p&gt;

&lt;p&gt;Because of that, I learned Three.js from the problems I actually had.&lt;/p&gt;

&lt;p&gt;I did not try to study the complete library before building. I learned what a renderer owns, how a scene and camera work, how geometry and materials are connected, why a render loop should stay under one owner, and why resources need to be cleaned up.&lt;/p&gt;

&lt;p&gt;That was enough to start checking the agent's decisions instead of only checking the final image.&lt;/p&gt;

&lt;p&gt;This was the point where the project changed from “AI generated this” to “I can explain why it is built this way.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What Astro, GSAP, and Three.js each did
&lt;/h2&gt;

&lt;p&gt;Keeping the responsibilities simple helped me understand the project.&lt;/p&gt;

&lt;p&gt;Astro owned the page structure, HTML content, and the split between desktop and mobile experiences.&lt;/p&gt;

&lt;p&gt;Three.js owned the canvas: the camera, terrain, tree, memory paths, particles, lighting, fog, and rendering.&lt;/p&gt;

&lt;p&gt;GSAP connected the scene to the visitor. One main scroll timeline changed camera movement, light, scene state, text, and progress as the user moved through the page.&lt;/p&gt;

&lt;p&gt;Webflow was mostly the deployment target. I was not using Webflow Designer to build the visual scene.&lt;/p&gt;

&lt;p&gt;This separation was important. GSAP changed values, but Three.js still owned the render loop. Astro rendered the real HTML story, but did not try to create the WebGL scene on the server.&lt;/p&gt;

&lt;p&gt;Once I understood those boundaries, I could give the agent better instructions and notice when a change mixed too many responsibilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance became a visual problem
&lt;/h2&gt;

&lt;p&gt;The first versions of the scene had too few lines and objects. The world looked empty and the central structure did not feel large enough.&lt;/p&gt;

&lt;p&gt;I kept asking for more: more paths, more waves, more detail, more particles, and a more complex silhouette.&lt;/p&gt;

&lt;p&gt;It worked visually until the page started to lag.&lt;/p&gt;

&lt;p&gt;This was the moment when I had to stop asking only for visual density and start thinking about what the camera could actually see.&lt;/p&gt;

&lt;p&gt;The scene did not need the same amount of detail everywhere. Objects close to the camera needed more detail because the visitor could see them. Distant objects could be simpler. Anything outside the useful view should not consume the same amount of work as the centre of the composition.&lt;/p&gt;

&lt;p&gt;The optimisation direction became:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use less detail in the distance;&lt;/li&gt;
&lt;li&gt;keep more detail near the camera;&lt;/li&gt;
&lt;li&gt;avoid rendering work that does not affect the visible frame;&lt;/li&gt;
&lt;li&gt;reuse geometry and materials where possible;&lt;/li&gt;
&lt;li&gt;do not create new objects during every frame;&lt;/li&gt;
&lt;li&gt;cap rendering resolution instead of following the full device pixel ratio;&lt;/li&gt;
&lt;li&gt;design complexity around the camera path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point was the most useful. This was not a free camera in a game. I knew where the visitor would be looking during every part of the scroll. The scene could spend its detail where it mattered.&lt;/p&gt;

&lt;p&gt;Performance was not separate from art direction. It changed how the artwork had to be composed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile needed a different answer
&lt;/h2&gt;

&lt;p&gt;Trying to keep the full desktop scene on phones would have created more performance problems and a worse experience.&lt;/p&gt;

&lt;p&gt;Instead, I made mobile a separate 2D passage. It uses a fixed image selected for the device theme, normal page scrolling, and real HTML text. It keeps the same idea and mood without loading the desktop Three.js experience.&lt;/p&gt;

&lt;p&gt;This was another useful lesson: AI often tries to reuse one implementation everywhere because it looks cleaner in code. Sometimes the better product has two simpler paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I had to give the agent
&lt;/h2&gt;

&lt;p&gt;The agent needed more than prompts.&lt;/p&gt;

&lt;p&gt;I gave it a product brief with the scene, story beats, interaction, visual direction, mobile behaviour, and things that were out of scope. I kept technical rules in the repository: one renderer, one animation loop, a limited pixel ratio, reduced-motion support, cleanup, and no Three.js on mobile.&lt;/p&gt;

&lt;p&gt;I also gave it references and continuous visual feedback.&lt;/p&gt;

&lt;p&gt;The useful input was a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the idea and mood;&lt;/li&gt;
&lt;li&gt;reference images;&lt;/li&gt;
&lt;li&gt;a clear description of the camera journey;&lt;/li&gt;
&lt;li&gt;boundaries between Astro, GSAP, and Three.js;&lt;/li&gt;
&lt;li&gt;official documentation;&lt;/li&gt;
&lt;li&gt;performance and accessibility rules;&lt;/li&gt;
&lt;li&gt;feedback from the real page after every meaningful change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this context, the agent could still generate code. It just could not know which result I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;AI lets you start with a technology before you know it deeply. Switching models could change how quickly I reached a useful direction, but no model removed the need to judge the output. That is real, and it is useful.&lt;/p&gt;

&lt;p&gt;But it does not remove the cost of not knowing the technology.&lt;/p&gt;

&lt;p&gt;You pay for that gap through more iterations, more explanations, more tokens, and more wrong directions. If UI or art direction is critical, you either need to know the tool or be ready to spend time teaching both yourself and the agent what the result should be.&lt;/p&gt;

&lt;p&gt;The agent can make a scene work. It cannot know where my line is between “technically correct” and “visually right.”&lt;/p&gt;

&lt;p&gt;My role was not to type every line of code. My role was to keep the idea clear, judge the result, turn visual problems into specific instructions, and gradually learn enough to review the important technical decisions.&lt;/p&gt;

&lt;p&gt;That is the main thing I took from this project.&lt;/p&gt;

&lt;p&gt;You do not need to understand everything before you begin. But you still need to look, question, test, and learn. AI can shorten the path between an idea and a working result. It does not decide what the result should feel like.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>astro</category>
      <category>gsap</category>
      <category>threejs</category>
    </item>
    <item>
      <title>Single Flight in Gleam: Managing Processes with OTP</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Sun, 20 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/single-flight-in-gleam-managing-processes-with-otp-h51</link>
      <guid>https://dev.to/andriishupta/single-flight-in-gleam-managing-processes-with-otp-h51</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/single_flight" rel="noopener noreferrer"&gt;SingleFlight repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/single_flight/blob/main/README.md" rel="noopener noreferrer"&gt;SingleFlight README&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/single_flight/blob/main/OTP_DESIGN.md" rel="noopener noreferrer"&gt;SingleFlight OTP design notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/single_flight/blob/main/lib/src/single_flight.gleam" rel="noopener noreferrer"&gt;Public API implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/single_flight/blob/main/lib/src/single_flight/internal/coordinator.gleam" rel="noopener noreferrer"&gt;Coordinator actor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/single_flight/blob/main/lib/src/single_flight/internal/flight.gleam" rel="noopener noreferrer"&gt;Flight actor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/single_flight/blob/main/lib/test/single_flight_test.gleam" rel="noopener noreferrer"&gt;Concurrency and cache tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gleam.run/documentation/" rel="noopener noreferrer"&gt;Gleam documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.erlang.org/doc/system/design_principles.html" rel="noopener noreferrer"&gt;Erlang/OTP design principles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/golang.org/x/sync/singleflight" rel="noopener noreferrer"&gt;Go singleflight package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I wanted to play with Gleam processes and OTP by building something more involved than a basic server. Single flight was a good fit: small enough to understand, but rich enough to explore actors, monitoring, supervision, retries, caching, and Erlang's “let it crash” approach.&lt;/p&gt;

&lt;p&gt;If 1,000 requests ask for the same expensive resource at the same time, the application should not make 1,000 identical calls. The first caller starts the work. Everyone else with the same operation and key joins it; synchronous callers receive the same outcome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,000 concurrent calls with the same operation and key
                    ↓
             one callback execution
                    ↓
  1,000 call(...) callers receive the same outcome

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not quite caching. A cache reuses completed work; single flight joins work that is still running. Different keys can still run concurrently, and two different operations do not collide even if they produce the same string key.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/andriishupta/single_flight" rel="noopener noreferrer"&gt;SingleFlight&lt;/a&gt; as a typed Gleam library for one BEAM node. The public API is small; most of the project is about deciding which process owns each piece of state and what happens when a process dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete example
&lt;/h2&gt;

&lt;p&gt;This is the smallest end-to-end shape, adapted from the tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
type Operations {
  Operations(
    get_user: single_flight.OperationDefinition(Int, String),
  )
}

pub fn main() -&amp;gt; Nil {
  let operations =
    Operations(
      get_user: single_flight.operation(
        key: int.to_string,
        run: fn(user_id) { "user-" &amp;lt;&amp;gt; int.to_string(user_id) },
      ),
    )

  let assert Ok(instance) =
    single_flight.new()
    |&amp;gt; single_flight.with_operations(operations)
    |&amp;gt; single_flight.start

  let assert Ok("user-7") =
    instance
    |&amp;gt; single_flight.use_operation(fn(operations) { operations.get_user })
    |&amp;gt; single_flight.call(7)

  Nil
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concurrent calls to &lt;code&gt;get_user&lt;/code&gt; with &lt;code&gt;7&lt;/code&gt; join one flight. A call with another user id gets a separate flight and can run at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The BEAM mental model
&lt;/h2&gt;

&lt;p&gt;BEAM processes do not share mutable state. Each process owns its state and receives messages through a mailbox. That changed the design question from “which lock protects this object?” to “which process owns this state?”:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Process&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coordinator&lt;/td&gt;
&lt;td&gt;Owns the map of active and cached keys.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flight actor&lt;/td&gt;
&lt;td&gt;Owns one key, its waiters, attempts, and retained result.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Callback worker&lt;/td&gt;
&lt;td&gt;Runs one attempt of user code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supervisors&lt;/td&gt;
&lt;td&gt;Own the runtime infrastructure and flight lifecycles.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gleam exposes these Erlang/OTP concepts through typed APIs. A &lt;code&gt;process.Subject(Message)&lt;/code&gt; is a typed address, an actor is a process with state and a message loop, a monitor reports process death as a message, and a supervisor defines recovery boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why operations live in an application record
&lt;/h2&gt;

&lt;p&gt;The shape of the API came from one requirement: selecting an operation must preserve its exact parameter and result types all the way to &lt;code&gt;call&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A regular &lt;code&gt;List&lt;/code&gt; or &lt;code&gt;Dict&lt;/code&gt; is homogeneous, so a straightforward runtime registry would either require every operation to share the same parameter and result types or erase them behind &lt;code&gt;Dynamic&lt;/code&gt; and cast later. Neither option gives the call site the contract I wanted.&lt;/p&gt;

&lt;p&gt;Instead, the application defines a record that acts like a typed operation interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type Operations {
  Operations(
    get_user: single_flight.OperationDefinition(Int, String),
    get_report: single_flight.OperationDefinition(Nil, Int),
  )
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field has its own &lt;code&gt;OperationDefinition(param, result)&lt;/code&gt;. It binds together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the parameter accepted by both &lt;code&gt;key&lt;/code&gt; and &lt;code&gt;run&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the result returned by &lt;code&gt;run&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a private identity used to scope concurrent work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation is correspondingly small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub opaque type SingleFlight(operations) {
  SingleFlight(config: Config, operations: operations)
}

pub opaque type OperationDefinition(param, result) {
  OperationDefinition(
    id: reference.Reference,
    key: fn(param) -&amp;gt; String,
    run: fn(param) -&amp;gt; result,
  )
}

pub opaque type Operation(param, result) {
  Operation(
    id: reference.Reference,
    coordinator_name: coordinator.Name,
    flight_supervisor_name: flight_supervisor.Name,
    key: fn(param) -&amp;gt; String,
    run: fn(param) -&amp;gt; result,
    settings: settings.Settings,
  )
}

pub fn operation(
  key key: fn(param) -&amp;gt; String,
  run run: fn(param) -&amp;gt; result,
) -&amp;gt; OperationDefinition(param, result) {
  OperationDefinition(id: reference.new(), key:, run:)
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The definition is opaque and process-free; creating one does not start an actor. The fields may have unrelated types: &lt;code&gt;get_user&lt;/code&gt; is &lt;code&gt;Int → String&lt;/code&gt;, while &lt;code&gt;get_report&lt;/code&gt; is &lt;code&gt;Nil → Int&lt;/code&gt;. The record gives them source-level names without forcing a common result type or storing application values as &lt;code&gt;Dynamic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The runtime itself carries the record type as &lt;code&gt;SingleFlight(operations)&lt;/code&gt;. &lt;code&gt;use_operation&lt;/code&gt; accepts a selector from that exact record and returns &lt;code&gt;Operation(param, result)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let get_user =
  instance
  |&amp;gt; single_flight.use_operation(fn(operations) { operations.get_user })

let result = single_flight.call(get_user, 7)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selector is the important part. When it returns &lt;code&gt;operations.get_user&lt;/code&gt;, Gleam infers both &lt;code&gt;Int&lt;/code&gt; and &lt;code&gt;String&lt;/code&gt;. The bound operation then makes &lt;code&gt;call&lt;/code&gt; accept only an &lt;code&gt;Int&lt;/code&gt; and return &lt;code&gt;Result(String, single_flight/error.Error)&lt;/code&gt;; no string operation name or result cast is involved.&lt;/p&gt;

&lt;p&gt;The public implementation shows that type flow directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn use_operation(
  instance instance: SingleFlight(operations),
  using select: fn(operations) -&amp;gt; OperationDefinition(param, result),
) -&amp;gt; Operation(param, result) {
  let SingleFlight(config:, operations:) = instance
  let OperationDefinition(id:, key:, run:) = select(operations)

  Operation(
    id:,
    coordinator_name: config.coordinator_name,
    flight_supervisor_name: config.flight_supervisor_name,
    key:,
    run:,
    settings: config.settings,
  )
}

pub fn call(
  operation operation: Operation(param, result),
  with param: param,
) -&amp;gt; Result(result, error.Error) {
  call_resolved(operation, param, operation.settings)
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Operation definitions should be created once and reused. Each call to &lt;code&gt;single_flight.operation&lt;/code&gt; also creates a new internal reference, so recreating an otherwise identical definition creates a separate namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key defines equivalent work
&lt;/h2&gt;

&lt;p&gt;The key function defines when two calls may share one result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;single_flight.operation(
  key: fn(user_id) { "github:user:" &amp;lt;&amp;gt; int.to_string(user_id) },
  run: github.get_user,
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the key omits a relevant input, unrelated requests may be collapsed. If it includes irrelevant changing data, useful deduplication is lost.&lt;/p&gt;

&lt;p&gt;Internally, the coordinator scopes the string key by the operation's private reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type ScopedKey =
  #(reference.Reference, String)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why two operations can both produce &lt;code&gt;"same-key"&lt;/code&gt; without sharing a flight or even sharing a result type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The process topology
&lt;/h2&gt;

&lt;p&gt;The runtime has two long-lived children and one temporary actor for every active or retained key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    OneForAll supervisor
                    / \
       FlightFactorySupervisor Coordinator
                 | |
          temporary children scoped key → flight
                 |
             FlightActor
                 |
       monitored callback worker

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within that tree, one acquisition flows like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;caller
  │ Acquire(operation id, key)
  ▼
Coordinator
  ├─ missing key → start a FlightActor
  └─ existing key → deliver another Run message
                              │
                              ▼
                         FlightActor
                          ├─ first Run → start one worker
                          └─ later Run → add a waiter

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The coordinator
&lt;/h3&gt;

&lt;p&gt;The coordinator owns routing state, not user work. Its mailbox serializes &lt;code&gt;Acquire&lt;/code&gt;, settlement, expiry, and child-down messages. That is the atomicity boundary: two callers cannot both observe a missing scoped key and create two owners for it.&lt;/p&gt;

&lt;p&gt;Its actor message loop stays small and delegates each state transition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fn handle_message(
  state state: State,
  message message: protocol.CoordinatorMessage,
) {
  case message {
    protocol.Acquire(key, operation_id, start, deliver, reply_to) -&amp;gt;
      handle_acquire(state, key, operation_id, start, deliver, reply_to)

    protocol.Settled(key, operation_id, pid, retention) -&amp;gt;
      handle_settled(state, key, operation_id, pid, retention)

    protocol.Expire(key, operation_id, pid) -&amp;gt;
      handle_expire(state, key, operation_id, pid)

    protocol.ChildDown(down) -&amp;gt; handle_child_down(state, down)
  }
  |&amp;gt; actor.continue
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The central acquisition branch is regular actor state handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;case dict.get(state.entries, scoped_key) {
  Error(Nil) -&amp;gt; start_flight(state, scoped_key, start, deliver, reply_to)

  Ok(entry) -&amp;gt; {
    let protocol.FlightHandle(pid:, ..) = entry.handle

    case process.is_alive(pid) {
      False -&amp;gt;
        state
        |&amp;gt; remove_entry(scoped_key)
        |&amp;gt; start_flight(scoped_key, start, deliver, reply_to)

      True -&amp;gt;
        case deliver(pid) {
          Ok(Nil) -&amp;gt; {
            process.send(reply_to, Ok(pid))
            touch_entry(state, scoped_key, entry)
          }
          Error(error.FlightUnavailable) -&amp;gt; {
            stop(entry.handle)
            state
            |&amp;gt; remove_entry(scoped_key)
            |&amp;gt; start_flight(scoped_key, start, deliver, reply_to)
          }
          Error(delivery_error) -&amp;gt; {
            process.send(reply_to, Error(delivery_error))
            state
          }
        }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a miss, &lt;code&gt;start_flight&lt;/code&gt; enforces the active-flight limit and asks the factory supervisor to start a child. On a hit, the coordinator sends the new waiter to the existing flight. &lt;code&gt;touch_entry&lt;/code&gt; updates LRU order when that entry is cached.&lt;/p&gt;

&lt;p&gt;Checking &lt;code&gt;is_alive&lt;/code&gt; is not enough by itself: the process can die before accepting &lt;code&gt;Run&lt;/code&gt;. Delivery therefore waits for an acknowledgement from the flight. If it returns &lt;code&gt;FlightUnavailable&lt;/code&gt;, the coordinator removes the stale entry and retries acquisition with a fresh flight.&lt;/p&gt;

&lt;p&gt;The coordinator also monitors every flight actor and removes its scoped entry when that child exits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The flight supervisor
&lt;/h3&gt;

&lt;p&gt;The factory supervisor has a smaller job: start and own flight actors. Its complete child configuration is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn supervised(name name: Name) {
  factory_supervisor.worker_child(fn(start_child) { start_child() })
  |&amp;gt; factory_supervisor.named(name)
  |&amp;gt; factory_supervisor.restart_strategy(supervision.Temporary)
  |&amp;gt; factory_supervisor.supervised
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Temporary&lt;/code&gt; is essential. A flight contains callbacks, waiters, attempts, and timers for one scoped key. Restarting it with empty state would not recover that work, so a crashed flight is removed and a later acquisition creates a new one.&lt;/p&gt;

&lt;p&gt;Four invariants keep the design understandable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Only the coordinator assigns a scoped key to a flight.&lt;/li&gt;
&lt;li&gt;One flight owns all attempts, waiters, and the retained result for that key.&lt;/li&gt;
&lt;li&gt;User code runs in a separate worker, never inside the coordinator or flight actor.&lt;/li&gt;
&lt;li&gt;Coordinator messages that remove or expire an entry must still refer to the same flight pid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last check matters because monitor notifications and expiry timers may arrive after a replacement flight has claimed the same logical key.&lt;/p&gt;

&lt;h2&gt;
  
  
  One flight is a state machine
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;First &lt;code&gt;Run&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Start one monitored worker and timeout timer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Running&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Another &lt;code&gt;Run&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Add a waiter without starting more work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Running&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;Ask the coordinator whether to retain or drop it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Running&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crash or timeout&lt;/td&gt;
&lt;td&gt;Retry, or settle one shared error.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RetryPending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry timer&lt;/td&gt;
&lt;td&gt;Start the next attempt.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finalizing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Retained&lt;/code&gt; or &lt;code&gt;Stop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reply to waiters, then keep or stop the actor.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed&lt;/td&gt;
&lt;td&gt;Another &lt;code&gt;Run&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Return the retained outcome immediately.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every attempt gets an id. Completion, timeout, and retry messages are accepted only when their id matches the current attempt, so a late message cannot settle newer work.&lt;/p&gt;

&lt;p&gt;The finalizing handshake is also deliberate. The flight waits until the coordinator has registered the retained entry or removed the dropped one before replying to callers. A new acquisition therefore cannot arrive while the result is published but key ownership is undecided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring, linking, and “let it crash”
&lt;/h2&gt;

&lt;p&gt;User callbacks run in unlinked processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let worker =
  process.spawn_unlinked(fn() {
    let result = operation()
    process.send(state.subject, OperationCompleted(id:, result:))
  })

let monitor = process.monitor(worker)
let timer =
  process.send_after(state.subject, timeout, OperationTimedOut(id:))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where “let it crash” becomes a design tool rather than a slogan. A broken callback is allowed to die, but its failure is contained. Because the worker is not linked to the flight, it cannot take the waiter-owning actor down with it. The monitor turns the death into a &lt;code&gt;WorkerDown&lt;/code&gt; message, which the flight can handle as &lt;code&gt;OperationFailed&lt;/code&gt; or retry.&lt;/p&gt;

&lt;p&gt;There are three outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;completion: cancel the timer and settle the result;&lt;/li&gt;
&lt;li&gt;crash: cancel the timer and handle &lt;code&gt;OperationFailed&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;timeout: stop monitoring, kill the worker, and handle &lt;code&gt;Timeout&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ownership boundary matters more than whether a process crashes: unsafe user code is disposable, while the process coordinating callers stays alive long enough to give them one shared outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries belong to the flight
&lt;/h2&gt;

&lt;p&gt;Retries cannot belong to individual callers. If five waiters retried independently, the implementation would stop being single flight. One flight owns one attempt sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let retrying =
  settings.new()
  |&amp;gt; settings.retry(
    max_retries: 2,
    delay: 100,
    on: settings.FailuresAndTimeouts,
  )
  |&amp;gt; single_flight.operation_settings(instance)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;max_retries: 2&lt;/code&gt; means at most three attempts including the first. Retries may target worker failures, timeouts, or both. All waiters remain attached to the same flight. A successful value is sent to all of them; a final runtime error is delivered only to checked waiters created by &lt;code&gt;call&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This does not guarantee exactly-once execution. A callback can complete an external side effect and then crash, or time out while external work continues. Side-effecting callbacks still need idempotency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supervision and recovery boundaries
&lt;/h2&gt;

&lt;p&gt;The coordinator and the factory supervisor run under &lt;code&gt;OneForAll&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;static_supervisor.new(static_supervisor.OneForAll)
|&amp;gt; static_supervisor.add(flight_supervisor.supervised(
  config.flight_supervisor_name,
))
|&amp;gt; static_supervisor.add(coordinator.supervised(
  config.coordinator_name,
  config.flight_supervisor_name,
  config.settings,
))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They form one consistency boundary. If only the coordinator restarted, it would forget live flights. If only the flight supervisor restarted, the coordinator could retain dead entries. Restarting both restores a consistent empty runtime.&lt;/p&gt;

&lt;p&gt;Together with the &lt;code&gt;Temporary&lt;/code&gt; flight policy shown earlier, this gives a clear recovery boundary: durable infrastructure restarts together, while disposable keyed work is recreated only when a caller asks for it again.&lt;/p&gt;

&lt;p&gt;An application with its own supervision tree can use &lt;code&gt;supervised&lt;/code&gt; and connect after the parent has started the returned child specification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let assert Ok(#(pending, single_flight_child)) =
  single_flight.new()
  |&amp;gt; single_flight.with_operations(operations)
  |&amp;gt; single_flight.supervised

let assert Ok(_) =
  static_supervisor.new(static_supervisor.OneForOne)
  |&amp;gt; static_supervisor.add(single_flight_child)
  |&amp;gt; static_supervisor.start

let assert Ok(instance) = single_flight.connect(pending)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;call&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;call&lt;/code&gt; waits for&lt;code&gt;Result(result, single_flight/error.Error)&lt;/code&gt;. It monitors the flight while waiting, so a dead flight becomes &lt;code&gt;FlightUnavailable&lt;/code&gt; instead of an endless receive.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;send&lt;/code&gt; accepts a subject and returns after the caller has joined the flight. The subject receives successful values only; runtime errors are not delivered to it. Use &lt;code&gt;call&lt;/code&gt; when the caller must observe failures such as &lt;code&gt;Timeout&lt;/code&gt; or &lt;code&gt;TooManyWaiters&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The library treats an application's own &lt;code&gt;Result&lt;/code&gt; as an ordinary successful value. If a callback returns &lt;code&gt;Result(User, ApiError)&lt;/code&gt;, SingleFlight errors still describe only the runtime around that callback.&lt;/p&gt;

&lt;p&gt;The first caller for a scoped key also defines that flight's callback and operation settings. Later callers join the active flight; they do not replace its retry, cache, timeout, or waiter policy.&lt;/p&gt;

&lt;p&gt;Each synchronous caller still has its own receive deadline. A caller may return &lt;code&gt;Timeout&lt;/code&gt; while a longer shared flight continues for other waiters. Version 1 has no caller cancellation, so its reply subject remains attached until that flight settles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching without erasing result types
&lt;/h2&gt;

&lt;p&gt;Active deduplication and completed-result caching share the same flight actor but solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the coordinator owns cache metadata, TTL timers, and LRU order;&lt;/li&gt;
&lt;li&gt;the retained flight owns the typed result in &lt;code&gt;Completed(result)&lt;/code&gt; or &lt;code&gt;CompletedError(error.Error)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps heterogeneous results out of a dynamic container. The coordinator only needs a pid, approximate size, expiry timer, and access order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settings.new()
|&amp;gt; settings.cache(
  success: settings.CacheFor(500),
  error: settings.NoCache,
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policies are &lt;code&gt;NoCache&lt;/code&gt;, &lt;code&gt;CacheFor(milliseconds)&lt;/code&gt;, and &lt;code&gt;CacheUntilEvicted&lt;/code&gt;. Instance-wide item and byte limits bound retained state. The byte size comes from Erlang external-term encoding, so it is an approximation rather than process-heap usage.&lt;/p&gt;

&lt;p&gt;Successes and SingleFlight runtime errors have separate retention policies. &lt;code&gt;Nil&lt;/code&gt; is cached like any other successful value.&lt;/p&gt;

&lt;p&gt;LRU and TTL eviction send &lt;code&gt;Stop&lt;/code&gt; to the retained flight. Expiry messages include both the operation identity and pid, preventing an old timer from deleting a replacement flight for the same key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backpressure
&lt;/h2&gt;

&lt;p&gt;BEAM can run many lightweight processes, so these settings are application limits rather than the VM's process limit. They make overload visible and predictable before memory, scheduler time, sockets, ports, or the downstream service become the real bottleneck.&lt;/p&gt;

&lt;p&gt;A slow key may collect many waiting subjects, while many distinct keys create a flight actor and callback worker each. The runtime therefore has separate controls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Bounds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_waiters&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Callers attached to one active flight.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;settings.max_flights&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Distinct keys executing at once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache item and byte limits&lt;/td&gt;
&lt;td&gt;Completed state retained by the runtime.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When the active-flight limit is reached, a new key receives &lt;code&gt;TooManyFlights&lt;/code&gt;, while a duplicate key may still join its existing flight.&lt;/p&gt;

&lt;p&gt;The useful number is not “how many processes can BEAM theoretically open?” It is how much concurrent work this application and its dependencies can handle. &lt;code&gt;TooManyWaiters&lt;/code&gt; and &lt;code&gt;TooManyFlights&lt;/code&gt; provide explicit failure paths instead of letting load grow until another resource fails.&lt;/p&gt;

&lt;p&gt;Per-operation handles may change timeout, retry, cache, and waiter policies. Active-flight and cache resource limits belong to the already running instance, so creating another handle cannot resize shared runtime capacity.&lt;/p&gt;

&lt;p&gt;The current implementation coordinates one BEAM node. A future distributed version could route a scoped key to one owner node and spread different keys across the cluster. That requires real distributed ownership and failure handling; simply hashing a key or increasing the worker count would not prevent duplicate execution during node failures or inconsistent cluster views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Working with several processes and supervisors required a better understanding of how the BEAM actually behaves: who owns state, how messages are ordered, what links and monitors do, and which process a supervisor should restart.&lt;/p&gt;

&lt;p&gt;“Let it crash” does not mean ignoring failures. Here it means running user code in an unlinked worker, monitoring it, and keeping the flight actor alive to handle the result, timeout, crash, or retry for its callers. The coordinator and supervisors then handle recovery at the level where state can be rebuilt safely.&lt;/p&gt;

&lt;p&gt;Single flight was a useful exercise because the public idea is small, while the implementation touches most of these process boundaries without needing a large application around it.&lt;/p&gt;

</description>
      <category>gleam</category>
      <category>erlang</category>
      <category>otp</category>
      <category>actors</category>
    </item>
    <item>
      <title>Gono - an experimental Hono wrapper in Gleam</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/gono-an-experimental-hono-wrapper-in-gleam-4na</link>
      <guid>https://dev.to/andriishupta/gono-an-experimental-hono-wrapper-in-gleam-4na</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/gono" rel="noopener noreferrer"&gt;Gono repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hono.dev/docs" rel="noopener noreferrer"&gt;Hono documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gleam.run/documentation/externals/" rel="noopener noreferrer"&gt;Gleam externals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gleam-javascript.hexdocs.pm/gleam/javascript/promise.html" rel="noopener noreferrer"&gt;&lt;code&gt;gleam/javascript/promise&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tour.gleam.run/functions/pipelines/" rel="noopener noreferrer"&gt;Gleam pipelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tour.gleam.run/advanced-features/use/" rel="noopener noreferrer"&gt;Gleam &lt;code&gt;use&lt;/code&gt; expressions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built Gono - Experimentation 🟡
&lt;/h2&gt;

&lt;p&gt;I wanted to learn Gleam by building something real.&lt;/p&gt;

&lt;p&gt;Not a large product or a new web framework. Just a small library that would force me to go beyond basic language examples.&lt;/p&gt;

&lt;p&gt;I chose &lt;a href="https://hono.dev/" rel="noopener noreferrer"&gt;Hono&lt;/a&gt; because I already understood its API. The experiment became &lt;a href="https://github.com/andriishupta/gono" rel="noopener noreferrer"&gt;Gono&lt;/a&gt;: a Gleam wrapper around Hono for JavaScript runtimes.&lt;/p&gt;

&lt;p&gt;My goals were simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;learn Gleam through working code;&lt;/li&gt;
&lt;li&gt;understand package and library design;&lt;/li&gt;
&lt;li&gt;use the Foreign Function Interface, or FFI;&lt;/li&gt;
&lt;li&gt;connect typed Gleam code with an existing JavaScript library.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The wrapper eventually worked. I could define routes in Gleam, run the application on Node or Bun, use middleware, and test requests without starting a real server.&lt;/p&gt;

&lt;p&gt;But the most useful lesson was not about routing. It was about the boundary between two languages.&lt;/p&gt;

&lt;p&gt;Calling JavaScript from Gleam is easy. Making every value and failure cross that boundary in a predictable form takes much more care.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gleam application
  → public Gono API
  → JavaScript FFI
  → Hono

Hono values and JavaScript failures
  → FFI conversion
  → Option / Result / Promise / custom types
  → Gleam application

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This article shows the parts of Gono that helped me understand that flow. The project is still experimental, but the same lessons apply to many JavaScript wrappers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full example
&lt;/h2&gt;

&lt;p&gt;Before looking at the wrapper internals, this is what a small Gono application looks like. It combines the same APIs covered by the project tests: a base path, middleware, request context reads, response context writes, and multiple routes.&lt;/p&gt;

&lt;p&gt;Because this example uses the Node runtime, the application must have &lt;code&gt;hono&lt;/code&gt; and &lt;code&gt;@hono/node-server&lt;/code&gt; installed through its &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/main.gleam&lt;/code&gt; can then define and start the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.with_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.with_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.new&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;instance&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.use_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.set_res_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-api-version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="n"&gt;c&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.next&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.continue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.with_base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.req&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.req_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="n"&gt;c&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="nf"&gt;.object&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="nf"&gt;.string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="nf"&gt;.string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Example user"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;]))&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.reply&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;c&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="nf"&gt;.object&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="nf"&gt;.string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"usr_123"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="nf"&gt;.bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;]))&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.reply&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;api&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="py"&gt;.builder&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="py"&gt;.serve&lt;/span&gt;

  &lt;span class="n"&gt;Nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middleware runs before both endpoints and writes a response header. &lt;code&gt;GET /api/v1/users/:id&lt;/code&gt; reads a path parameter from the request context, while &lt;code&gt;POST /api/v1/users&lt;/code&gt; changes the response status and returns JSON. The rest of the article explains the wrapper decisions behind this API.&lt;/p&gt;

&lt;h2&gt;
  
  
  A binding is not yet a good wrapper
&lt;/h2&gt;

&lt;p&gt;A basic binding answers one question: how can Gleam call this JavaScript function?&lt;/p&gt;

&lt;p&gt;A useful wrapper also decides what the JavaScript API should feel like in Gleam. This matters when the original library uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mutable instances;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;callbacks with several possible outcomes;&lt;/li&gt;
&lt;li&gt;promises that can reject with any JavaScript value;&lt;/li&gt;
&lt;li&gt;runtime-specific resources such as Node and Bun servers;&lt;/li&gt;
&lt;li&gt;variadic functions and JavaScript arrays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hono uses all of these patterns. Gono therefore needed more than direct bindings. It needed its own application type, handler and middleware results, runtime errors, and pipeline-friendly functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep foreign values opaque
&lt;/h2&gt;

&lt;p&gt;The first decision was how much Gleam should know about a Hono object.&lt;/p&gt;

&lt;p&gt;Gono does not try to model all of Hono's internal state in Gleam. It declares the value as an opaque foreign type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Hono&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type has no constructors that a Gono user can call. A value of type &lt;code&gt;Hono&lt;/code&gt; can only come from the wrapper. The same approach is used for Hono's request context and response objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nf"&gt;GonoContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;GonoRequest&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;GonoResponse&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript creates and operates on these values. Gleam controls where they can be used. A caller cannot create a fake Hono context or depend on its internal fields.&lt;/p&gt;

&lt;p&gt;The JavaScript side can document the real shape with JSDoc:&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="cm"&gt;/**
 * @typedef {import("hono").Hono} Hono
 * @typedef {import("hono").Context} Context
 */&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSDoc gives JavaScript tooling useful information, while the Gleam declaration protects callers on the Gleam side. Neither validates the runtime boundary by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define small FFI functions
&lt;/h2&gt;

&lt;p&gt;An external function has no Gleam body. &lt;code&gt;@external&lt;/code&gt; tells Gleam which JavaScript function to call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;external&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;javascript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"./gono/gono_ffi.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"new_hono"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new_hono&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type annotation is required, but Gleam cannot verify the JavaScript implementation. It cannot prove that the function exists or returns the declared type.&lt;/p&gt;

&lt;p&gt;The JavaScript implementation must return the representation expected by compiled Gleam code:&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$gleam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$gleam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`GONO_ERROR_NEW:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper keeps the raw string error private and maps it to a Gleam error type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;GonoError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;GonoErrorMissingModule&lt;/span&gt;
  &lt;span class="n"&gt;GonoErrorUnsupportedVersion&lt;/span&gt;
  &lt;span class="nf"&gt;GonoErrorNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;GonoErrorUnknown&lt;/span&gt;
  &lt;span class="nf"&gt;GonoErrorRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GonoBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Gono&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GonoError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;new_hono&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hono&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Gono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hono&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="py"&gt;.host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="py"&gt;.port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.map_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string_error_to_gono_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives callers a normal Gleam &lt;code&gt;Result&lt;/code&gt; instead of exposing a JavaScript exception. The declaration describes the contract; the JavaScript code still has to keep it at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the public API natural in Gleam
&lt;/h2&gt;

&lt;p&gt;I did not want users to configure Gono through a JavaScript-shaped object. The configuration is normal Gleam code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;opaque&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;GonoBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;GonoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GonoBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;GonoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GonoBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GonoBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;GonoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GonoBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GonoBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;GonoBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The builder functions return a new Gleam value. They do not mutate a configuration object and they are designed for the pipe operator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.with_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.with_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.new&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting &lt;code&gt;Gono&lt;/code&gt; value contains both the foreign Hono application and the runtime configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;opaque&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Gono&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;Gono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hono&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance keeps configuration beside the Hono object without exposing its internal shape. It also separates building configuration from creating the foreign object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap mutation in a pipeline-friendly API
&lt;/h2&gt;

&lt;p&gt;Hono's registration methods mutate an application and return the same Hono instance:&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;return&lt;/span&gt; &lt;span class="nx"&gt;app&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="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handlerOrMiddleware&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gono keeps this behavior inside JavaScript, but presents a pipeline API in Gleam:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.Gono&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoContextHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.Gono&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;instance&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.get_hono&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;app_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;unwrap_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.set_hono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gono returns a reconstructed wrapper after registration, while Hono keeps mutating its application object. This is not real immutability. It is a pipeline-friendly interface around controlled JavaScript mutation.&lt;/p&gt;

&lt;p&gt;Gono includes a small reference-identity test to ensure rebuilding the wrapper does not clone the foreign object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hono&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.get_hono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.set_hono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hono&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.hono_same_ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.get_hono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.get_hono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt;&lt;span class="py"&gt;.be_true&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API reads functionally, but registering a route still changes Hono. The wrapper improves composition; it does not pretend that Hono works differently.&lt;/p&gt;

&lt;p&gt;The result is readable route composition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;instance&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.reply&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gleam's pipe operator passes the value on the left as the first argument to the next function. Put the wrapper's main subject first so callers can compose operations naturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make handler results explicit
&lt;/h2&gt;

&lt;p&gt;Hono handlers may return a response immediately or through a promise. Gono makes those two cases visible in the type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nf"&gt;GonoHandlerReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;GonoReplySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;GonoReplyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nf"&gt;GonoContextHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;GonoContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GonoHandlerReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helpers make both branches explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoHandlerReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;reply_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoHandlerReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter then normalizes both forms into the JavaScript contract, which is a promise of a response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;unwrap_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoContextHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GonoFfiContextHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;context_bind_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller can see that a handler may finish now or later. The adapter converts both cases into the promise-based shape expected by Hono.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put policy in Gleam
&lt;/h2&gt;

&lt;p&gt;Most JavaScript functions in Gono are deliberately small:&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;return&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper's policy stays in Gleam. For example, a custom not-found handler defaults to &lt;code&gt;404&lt;/code&gt; only when the response still has status &lt;code&gt;200&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_default_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_default_not_found_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoContextHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoContextHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;with_default_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.GonoReplySync&lt;/span&gt;

      &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;response_promise&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;with_default_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.GonoReplyAsync&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This became an important rule for Gono:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript handles mechanics that require Hono or runtime objects.&lt;/li&gt;
&lt;li&gt;Gleam owns defaults, variants, error mapping, and composition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The JavaScript file stays focused on interop. The behavior that Gono owns remains visible and testable in Gleam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model middleware flow instead of hiding it
&lt;/h2&gt;

&lt;p&gt;Hono middleware has two useful outcomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;call &lt;code&gt;next()&lt;/code&gt; and let the downstream handler continue;&lt;/li&gt;
&lt;li&gt;return a response early and stop the chain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Gono represents that distinction directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nf"&gt;GonoMiddlewareReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;GonoReplyNext&lt;/span&gt;
  &lt;span class="nf"&gt;GonoReplyAsyncNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;GonoHandlerReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nf"&gt;GonoMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;GonoContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;GonoMiddlewareReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context helpers cover continuing the chain and returning either a synchronous or asynchronous response. Callers do not need to know what &lt;code&gt;undefined&lt;/code&gt; means to Hono middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoMiddlewareReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.GonoReplyNext&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="py"&gt;.resolve&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;reply_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoMiddlewareReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.GonoReplyAsyncNext&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="py"&gt;.resolve&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;reply_async_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoMiddlewareReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reply_async&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.GonoReplyAsyncNext&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="py"&gt;.resolve&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A middleware that adds a header and continues can be written as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.set_res_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-request-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;c&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.next&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.continue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An early response is a different outcome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;forbidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;c&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"forbidden"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.reply_next&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter converts these outcomes to Hono's middleware contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;unwrap_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GonoFfiMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;context_bind_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;next_result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.GonoReplyNext&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="nf"&gt;.nil&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplyAsyncNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;handler_result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt;
              &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;as_dynamic&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="py"&gt;.resolve&lt;/span&gt;
            &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.GonoReplyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt;
              &lt;span class="n"&gt;response_promise&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_dynamic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also why &lt;code&gt;context.next&lt;/code&gt; is a wrapper function instead of a plain field access. The actual Hono &lt;code&gt;next&lt;/code&gt; callback is supplied by JavaScript and bound to the context before the Gleam handler runs:&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;const&lt;/span&gt; &lt;span class="nx"&gt;NEXT_SYMBOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gono.next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;NEXT_SYMBOL&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;NEXT_SYMBOL&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;next&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The symbol keeps this bridge private and reduces the chance of a property collision. Gleam users only see &lt;code&gt;context.next&lt;/code&gt;, not the mutable value attached to the JavaScript context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main lesson: contain JavaScript failures
&lt;/h2&gt;

&lt;p&gt;This was the most important lesson from the project.&lt;/p&gt;

&lt;p&gt;A JavaScript function can throw immediately. A promise can reject later. A server can start successfully and emit an error afterward. If one of these failures crosses the FFI boundary unchanged, it becomes much harder to reason about from Gleam.&lt;/p&gt;

&lt;p&gt;The Gleam JavaScript promise type deliberately does not include a generic error type because JavaScript can reject with any value. If an operation needs a typed success-or-failure contract, put the &lt;code&gt;Result&lt;/code&gt; inside the promise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a new external call, the boundary can look like this:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;external&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;javascript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./provider_ffi.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;load_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;



  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$gleam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$gleam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$gleam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the promise resolves in both cases. Success becomes &lt;code&gt;Ok&lt;/code&gt;, and a recoverable failure becomes &lt;code&gt;Error&lt;/code&gt;. Gleam receives the value described by the external function instead of an unexpected rejected promise.&lt;/p&gt;

&lt;p&gt;Gono uses this boundary selectively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;new_hono&lt;/code&gt; catches construction failures and returns a Gleam &lt;code&gt;Result&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Node and Bun server creation catch runtime failures and map them to runtime errors.&lt;/li&gt;
&lt;li&gt;URL query parsing catches invalid URL failures and returns &lt;code&gt;Error(Nil)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The mock request helper is intentionally a small async bridge used by tests.&lt;/li&gt;
&lt;li&gt;Hono route and middleware promises can still reject; Hono catches handler and middleware errors and sends them to &lt;code&gt;onError&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not mean adding &lt;code&gt;try/catch&lt;/code&gt; around everything. Catch a failure when the wrapper can turn it into a useful value. If Hono already owns that failure path, preserve Hono's behavior instead of swallowing it.&lt;/p&gt;

&lt;p&gt;For a promise that already contains a &lt;code&gt;Result&lt;/code&gt;, &lt;code&gt;promise.try_await&lt;/code&gt; continues the callback only for the &lt;code&gt;Ok&lt;/code&gt; branch. The surrounding function still returns a promise of a result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_example&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.try_await&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the error needs to be handled at that point, use &lt;code&gt;promise.await&lt;/code&gt; and match on the result explicitly.&lt;/p&gt;

&lt;p&gt;For callback-based APIs, adapt the callback once with &lt;code&gt;promise.new&lt;/code&gt;, then keep the rest of the flow in Gleam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert values once, at the edge
&lt;/h2&gt;

&lt;p&gt;Gleam lists and JavaScript arrays are different runtime representations. Gono converts them in the FFI rather than making every public function expose JavaScript-specific details:&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;function&lt;/span&gt; &lt;span class="nf"&gt;middlewares_to_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;middlewares&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;middlewares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;$gleam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;converted&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;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That helper lets JavaScript call Hono's variadic API:&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;return&lt;/span&gt; &lt;span class="nx"&gt;app&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="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;middlewares_to_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;middlewares&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reverse conversion is needed for values returned from JavaScript:&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;const&lt;/span&gt; &lt;span class="nx"&gt;headerEntries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list_to_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://localhost&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headerEntries&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responseHeaders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$gleam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseHeaders&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public Gleam test API can therefore use normal Gleam collections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;runtime_mock&lt;/span&gt;&lt;span class="nf"&gt;.request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="py"&gt;.Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle applies to optional values. Hono may return &lt;code&gt;undefined&lt;/code&gt; for a missing parameter or header, so Gono converts it to &lt;code&gt;option.Option&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;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="nc"&gt;$None&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="nc"&gt;$Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&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="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a Gleam caller can decide how to handle absence instead of receiving &lt;code&gt;undefined&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;c&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.req&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.req_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"missing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code depends on Gleam's generated JavaScript representation, such as &lt;code&gt;$gleam.Empty&lt;/code&gt; and &lt;code&gt;$gleam.toList&lt;/code&gt;. I keep that dependency inside the FFI module so the rest of Gono does not need to know about it. It is also an obvious place to re-test after a Gleam upgrade.&lt;/p&gt;

&lt;p&gt;Convert values once, close to the FFI, and keep the rest of the wrapper idiomatic Gleam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate Node and Bun
&lt;/h2&gt;

&lt;p&gt;The Hono application is JavaScript-runtime agnostic, but starting a server is not. Gono keeps Node and Bun adapters in separate modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;instance&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="py"&gt;.builder&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="nf"&gt;.on_process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SIGTERM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Nil&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="py"&gt;.serve&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Node FFI checks the environment before calling the Node adapter:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_node_env&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NODE_ERROR_INVALID_ENV&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Register process and server events here.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="nc"&gt;$Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`NODE_ERROR_SERVER:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gleam module maps those strings into a closed error type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;NodeError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;NodeErrorInvalidEnv&lt;/span&gt;
  &lt;span class="n"&gt;NodeErrorUnsupportedVersion&lt;/span&gt;
  &lt;span class="nf"&gt;NodeErrorServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;NodeErrorUnknown&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;try/catch&lt;/code&gt; only handles failures during setup. A Node server can emit an error after it has started, so Gono also registers an &lt;code&gt;error&lt;/code&gt; event listener. These are separate failure paths and both need handling.&lt;/p&gt;

&lt;p&gt;This keeps Node-specific behavior out of the core wrapper. Gono uses the same structure for Bun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use tests as documentation
&lt;/h2&gt;

&lt;p&gt;The Gono tests show how I expect the wrapper to be used. They also verify the part the Gleam compiler cannot see: whether JavaScript really returns the promised representation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the instance boundary
&lt;/h3&gt;

&lt;p&gt;The instance tests verify defaults, builder overrides, and the identity of the foreign Hono object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;gono_defaults_test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.new&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt;&lt;span class="py"&gt;.be_ok&lt;/span&gt;

  &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.get_host&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt;&lt;span class="nf"&gt;.equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.get_port&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt;&lt;span class="nf"&gt;.equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test routes through the mock runtime
&lt;/h3&gt;

&lt;p&gt;The mock adapter executes the actual Hono app in memory. That makes route tests fast and avoids binding a network port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;hello_route_test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="nf"&gt;.builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gono&lt;/span&gt;&lt;span class="py"&gt;.new&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;instance&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="py"&gt;.reply&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="n"&gt;runtime_mock&lt;/span&gt;&lt;span class="nf"&gt;.request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="py"&gt;.Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt;&lt;span class="nf"&gt;.equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="na"&gt;.1&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt;&lt;span class="nf"&gt;.equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Nil&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test middleware outcomes separately
&lt;/h3&gt;

&lt;p&gt;Gono's context tests cover both sides of the middleware contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a middleware can mutate a header and continue to the handler;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reply_next&lt;/code&gt; can stop the chain synchronously;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reply_async_next&lt;/code&gt; can stop it with a promise response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difficult part of a wrapper is often not the direct method call. It is preserving callback order and return-value semantics across two languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep long-running server tests out of &lt;code&gt;gleeunit&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Gleeunit exits the process when test execution finishes. A test that starts a real Node or Bun server should use a dedicated integration or end-to-end runner instead of relying on the unit-test process to stay alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would repeat
&lt;/h2&gt;

&lt;p&gt;If I write another JavaScript wrapper in Gleam, I will follow the same rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the smallest foreign values that need to cross the boundary.&lt;/li&gt;
&lt;li&gt;Represent them as opaque Gleam types.&lt;/li&gt;
&lt;li&gt;Give every external function an explicit type annotation.&lt;/li&gt;
&lt;li&gt;Convert JavaScript &lt;code&gt;undefined&lt;/code&gt; and nullable values to &lt;code&gt;Option&lt;/code&gt; or &lt;code&gt;Result&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Convert lists and records at the FFI edge.&lt;/li&gt;
&lt;li&gt;Use custom types for meaningful sync, async, continue, and early-reply outcomes.&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;try/catch&lt;/code&gt; around JavaScript operations that can fail synchronously.&lt;/li&gt;
&lt;li&gt;Handle later callback, event, and promise failures through their own channels.&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;Promise(Result(...))&lt;/code&gt; when an async operation has a typed error contract.&lt;/li&gt;
&lt;li&gt;Keep runtime-specific adapters separate from the core wrapper.&lt;/li&gt;
&lt;li&gt;Test the contract the compiler cannot see: foreign identity, callback order, promise behavior, value conversion, and error paths.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I got from the experiment
&lt;/h2&gt;

&lt;p&gt;Gono is a small project. It was never meant to replace Hono or become a large framework.&lt;/p&gt;

&lt;p&gt;It did what I needed: the wrapper worked, I could run it on Node and Bun, and the tests described a usable Gleam API around Hono.&lt;/p&gt;

&lt;p&gt;More importantly, it changed how I think about interop. The happy-path call is usually the easy part. The real work is deciding what happens to mutation, missing values, callbacks, exceptions, rejected promises, and runtime errors when they move between languages.&lt;/p&gt;

&lt;p&gt;An FFI type is a promise to the compiler, not proof about JavaScript at runtime. The boundary becomes reliable only when the JavaScript implementation, Gleam types, and tests all describe the same behavior.&lt;/p&gt;

&lt;p&gt;That lesson was more valuable than the wrapper itself.&lt;/p&gt;

</description>
      <category>gleam</category>
      <category>javascript</category>
      <category>ffi</category>
      <category>hono</category>
    </item>
    <item>
      <title>Connect Polkadot to a Next.js website with @polkadot/extension-dapp</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Sat, 17 Dec 2022 09:34:11 +0000</pubDate>
      <link>https://dev.to/andriishupta/connect-polkadot-to-a-nextjs-website-with-polkadotextension-dapp-55d1</link>
      <guid>https://dev.to/andriishupta/connect-polkadot-to-a-nextjs-website-with-polkadotextension-dapp-55d1</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/andriishupta" rel="noopener noreferrer"&gt;andriishupta/polkadot-extension-dapp-example | GitHub&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://polkadot-extension-dapp-example.vercel.app" rel="noopener noreferrer"&gt;polkadot-extension-dapp-example | Deployed on Vercel&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://polkadot.js.org/docs/extension/" rel="noopener noreferrer"&gt;Extension Docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://subwallet.app/" rel="noopener noreferrer"&gt;SubWallet&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/dappforce/polkaverse" rel="noopener noreferrer"&gt;PolkaVerse | GitHub&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/andriishupta/subtips-app" rel="noopener noreferrer"&gt;Subtips | Github&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🌴 &lt;a href="https://linktr.ee/andriishupta" rel="noopener noreferrer"&gt;My Links&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📰 Published on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.andriishupta.dev" rel="noopener noreferrer"&gt;Hashnode 💻&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://andriishupta.medium.com" rel="noopener noreferrer"&gt;Medium ✍️&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/andriishupta"&gt;DEV Community 👩‍💻👨‍💻&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤓 Intro
&lt;/h2&gt;

&lt;p&gt;As a developer, it's essential to understand the differences between various blockchain platforms to choose the right one for your needs. One key difference(as for me) is the availability of technical resources and tutorials for developers. Ethereum has many articles and tutorials demonstrating how to connect a wallet to a website, whereas there is less information available on Polkadot. This may make it easier for developers who are new to Ethereum to get started and learn how to build applications on the platform.&lt;/p&gt;

&lt;p&gt;It's important for developers also to be familiar with Polkadot and how to build on its platform, as it has unique features and potential applications.&lt;/p&gt;

&lt;p&gt;Developer Experience outside just documentation is quite important nowadays for the web3 community.&lt;/p&gt;

&lt;p&gt;In this article, I will show an example of how to connect a &lt;a href="https://subwallet.app" rel="noopener noreferrer"&gt;SubWallet&lt;/a&gt;(that's what I have used) to a Next.js website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technology
&lt;/h3&gt;

&lt;p&gt;On Ethereum, we have web3.js and ethers.js for connecting to a website and many different libraries built on top of it. For example, many projects I have seen use &lt;a href="https://wagmi.sh/" rel="noopener noreferrer"&gt;wagmi&lt;/a&gt; with React.js. It is just a blessing, such as it includes a full list of functionality that you need to have to interact with blockchain: "Connect Wallet" display ENS and balances information, sign messages, interact with contracts, and much more —&amp;nbsp;all with caching, request deduplication, and persistence.&lt;/p&gt;

&lt;p&gt;Polkadot has production-ready libraries and tools to work with blockchain, and one of them is &lt;code&gt;@polkadot/extension-dapp&lt;/code&gt;. That's what we would use to "Connect Wallet".&lt;/p&gt;

&lt;h2&gt;
  
  
  🧑‍💻 Coding
&lt;/h2&gt;

&lt;p&gt;We will use the default Next.js app, so nothing is new here. Check out &lt;a href="https://nextjs.org/docs/getting-started" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; to remind yourself of Next.js.&lt;/p&gt;

&lt;p&gt;The most crucial point with &lt;code&gt;@polkadot/extension-dapp&lt;/code&gt; is that it needs a browser to run, so with Next.js, we need to render the "Connect" button only during Client-Side rendering. For that, we would use &lt;a href="https://nextjs.org/docs/advanced-features/dynamic-import" rel="noopener noreferrer"&gt;dynamic import&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/andriishupta/polkadot-extension-dapp-example/blob/main/pages/index.tsx" rel="noopener noreferrer"&gt;🔗 source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flrwtydlet4m0p9w3egbh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flrwtydlet4m0p9w3egbh.png" alt="dynamic-import" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Home page, we load our &lt;code&gt;Connect&lt;/code&gt; component dynamically. Connect components are where all magic happens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/andriishupta/polkadot-extension-dapp-example/blob/main/components/Connect.tsx" rel="noopener noreferrer"&gt;🔗 source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's start with imports:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lc5sipgup7d80j83h9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lc5sipgup7d80j83h9f.png" alt="polkadot-imports" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;InjectedAccountWithMeta&lt;/code&gt; is a type of account that we would get. I imported it for TypeScript.&lt;/p&gt;

&lt;p&gt;Our main focus here is &lt;code&gt;web3Enable&lt;/code&gt; and &lt;code&gt;web3Accounts&lt;/code&gt; .&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I have used promise chaining down below, but if you prefer&lt;/em&gt; &lt;code&gt;try-catch&lt;/code&gt; &lt;em&gt;+&lt;/em&gt; &lt;code&gt;async / await&lt;/code&gt; &lt;em&gt;- go for it!&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;web3Enable&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7wcqnnbwntezoj01m67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7wcqnnbwntezoj01m67.png" alt="web3Enable" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The initial function is to call even to check if our browser has any wallets to work with. In case we don't have anything to work with, the extension shows it in the console, and we also should throw an error to show the user that he should use the browser with a valid Wallet.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;web3Accounts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixohuzesqlzn71wj4gdr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixohuzesqlzn71wj4gdr.png" alt="web3Accounts" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same with &lt;code&gt;web3Accounts&lt;/code&gt; - it would load accounts connected or prompt you to connect to the website if you opened it for the first time - a very familiar experience for web3 users.&lt;/p&gt;

&lt;p&gt;You could try out the flow on &lt;a href="https://polkadot-extension-dapp-example.vercel.app" rel="noopener noreferrer"&gt;polkadot-extension-dapp-example | Deployed on Vercel&lt;/a&gt;, and it looks like this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I have tried only when I have 1 wallet, but for a simple example, it should be enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsy7gbl9mfbn00ny8jle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsy7gbl9mfbn00ny8jle.png" alt="connect" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kf28do3g6lhoc7a5qoe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kf28do3g6lhoc7a5qoe.png" alt="connecting" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjodd1a889oif47fgsed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjodd1a889oif47fgsed.png" alt="hello" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7ihis37zzvgo1bvhncw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7ihis37zzvgo1bvhncw.png" alt="error" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgdgdp9yqszxd9ejzxjd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgdgdp9yqszxd9ejzxjd.png" alt="logs" width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🧑‍🏫 Next steps and more examples
&lt;/h2&gt;

&lt;p&gt;With a connected wallet and available account, you can do everything you want: check out how to create and sign a transaction, show &lt;code&gt;&amp;lt;Identicon/&amp;gt;&lt;/code&gt; and more:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://polkadot.js.org/docs" rel="noopener noreferrer"&gt;https://polkadot.js.org/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A more mature example using &lt;code&gt;@polkadot/api&lt;/code&gt; , &lt;code&gt;@polkadot/ui-keyring&lt;/code&gt; and more is &lt;a href="https://github.com/dappforce/polkaverse" rel="noopener noreferrer"&gt;PolkaVerse&lt;/a&gt;. It is a decentralized social network built on &lt;a href="https://subsocial.network/" rel="noopener noreferrer"&gt;Subsocial&lt;/a&gt; - "The Blockchain for Social Finance".&lt;/p&gt;

&lt;p&gt;Also, I have another project where I tried to use Subsocial API - it is raw-raw-raw cause it was fine for me to connect to Subsocial API and Polkadot to experience how things work. Feel free to check it out: &lt;a href="https://github.com/andriishupta/subtips-app" rel="noopener noreferrer"&gt;https://github.com/andriishupta/subtips-app&lt;/a&gt;&lt;/p&gt;










&lt;p&gt;Thanks for reading! 🙇&lt;/p&gt;

</description>
      <category>polkadot</category>
      <category>web3</category>
      <category>nextjs</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Simplify usage of Lens API with @use-lens and graphql-codegen</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Tue, 04 Oct 2022 11:05:21 +0000</pubDate>
      <link>https://dev.to/andriishupta/simplify-usage-of-lens-api-with-use-lens-and-graphql-codegen-5fk1</link>
      <guid>https://dev.to/andriishupta/simplify-usage-of-lens-api-with-use-lens-and-graphql-codegen-5fk1</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta" rel="noopener noreferrer"&gt;andriishupta | Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/use-lens/use-lens" rel="noopener noreferrer"&gt;Use Lens | Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.lens.xyz" rel="noopener noreferrer"&gt;Lens API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://the-guild.dev/graphql/codegen" rel="noopener noreferrer"&gt;GraphQL Code Generator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📰 Published on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.andriishupta.dev" rel="noopener noreferrer"&gt;Hashnode 💻&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andriishupta.medium.com" rel="noopener noreferrer"&gt;Medium ✍️&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/andriishupta"&gt;DEV Community 👩‍💻👨‍💻&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤓 Intro
&lt;/h2&gt;

&lt;p&gt;Recently, I have used Lens API to build some playground apps and noticed a repetitive task: I created Lens API Queries and Mutations for every project and used &lt;a href="https://the-guild.dev/graphql/codegen" rel="noopener noreferrer"&gt;GraphQL Code Generator&lt;/a&gt; to use React hooks with Apollo Client. &lt;/p&gt;

&lt;p&gt;After 2nd time doing the same job, I decided to create a library for easier use of Lens API that would save me some time to do actual work.&lt;/p&gt;

&lt;p&gt;I have never created open-source packages and thought it would be a great experience to do it, even if only I would use it 😅.&lt;/p&gt;

&lt;p&gt;npm &lt;a href="https://www.npmjs.com/org/use-lens" rel="noopener noreferrer"&gt;@use-lens&lt;/a&gt; so far contains 2 packages - &lt;em&gt;CLI&lt;/em&gt; and &lt;em&gt;react-apollo&lt;/em&gt;. Later in this article, I will explain how to use them and when, and the same information could be found in the &lt;a href="https://github.com/use-lens/use-lens/#-usage" rel="noopener noreferrer"&gt;repo's README.md&lt;/a&gt;.&lt;br&gt;
I will explain how I see the best use of &lt;code&gt;@use-lens/*&lt;/code&gt; later in the article.&lt;/p&gt;

&lt;p&gt;Also, I liked Lens Protocol so much that I have created Github Organization &lt;a href="https://github.com/use-lens" rel="noopener noreferrer"&gt;Use Lens&lt;/a&gt;, where I plan to develop and publish some tools, examples and apps working on top of Lens.&lt;/p&gt;
&lt;h3&gt;
  
  
  Technology
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Lens Protocol is a composable and decentralized social graph, ready for you to build on so you can focus on creating a great experience, not scaling your users.&lt;/p&gt;

&lt;p&gt;Own your content. Own your social graph. Own your data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have read so far - you know what Lens API is and how to use it. If you are here to check out &lt;code&gt;@use-lens&lt;/code&gt; or just to check how to generate GraphQL code - educate yourself of Lens Protocol and API here:&lt;/p&gt;

&lt;p&gt;🌿 &lt;a href="https://docs.lens.xyz" rel="noopener noreferrer"&gt;https://docs.lens.xyz&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://the-guild.dev/graphql/codegen" rel="noopener noreferrer"&gt;GraphQL Code Generator&lt;/a&gt; - is a tool to build read-to-use code from your GraphQL schema and operations with a simple CLI with a lot of plugins for different frameworks: React, Next.js, Svelte, Vue with Apollo, URLQ and others - you name it.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧑‍💻 How to
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Generate with &lt;a href="https://github.com/use-lens/use-lens/tree/main/packages/cli" rel="noopener noreferrer"&gt;@use-lens/cli&lt;/a&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @use-lens/cli
use-lens generate %PACKAGE%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;with &lt;code&gt;npx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @use-lens/cli generate %PACKAGE%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would copy essential files of Lens API to your repo and would run &lt;code&gt;graphql-codegen&lt;/code&gt; to generate the code. By default, it would go to &lt;code&gt;src/lens-api/index.ts&lt;/code&gt;.&lt;br&gt;
From here, you could adjust &lt;code&gt;tsconfig.json&lt;/code&gt; to use it with &lt;code&gt;@use-lens&lt;/code&gt; shortening, so it would feel like a package usage. More on how to do it &lt;a href="https://github.com/use-lens/use-lens/tree/main/packages/cli#optional-tsconfigs-paths" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  With &lt;code&gt;@use-lens/*&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The simple &lt;code&gt;npm install --save @use-lens/%PACKAGE%&lt;/code&gt; and use it as a regular package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;GlobalProtocolStatsDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;GlobalProtocolStats&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;GlobalProtocolStatsType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useGlobalProtocolStatsQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useGlobalProtocolStatsLazyQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@use-lens/react-apollo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Manual GraphQL Code Generator
&lt;/h3&gt;

&lt;p&gt;The default approach is simple and could be followed by official docs &lt;a href="https://the-guild.dev/graphql/codegen/docs/getting-started" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On high-level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install &lt;code&gt;@graphql-codegen/cli&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;pick a plugin for your stack: for example, &lt;a href="https://www.the-guild.dev/graphql/codegen/plugins/typescript/typescript-react-apollo" rel="noopener noreferrer"&gt;react-apollo&lt;/a&gt; and install it&lt;/li&gt;
&lt;li&gt;get your GraphQL Schema and Documents&lt;/li&gt;
&lt;li&gt;create basic &lt;code&gt;codegen.yml&lt;/code&gt; for &lt;code&gt;graphql-codegen&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;graphql-codegen&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In your &lt;code&gt;codegen.yml&lt;/code&gt;, you should specify schema, documents, where to save, and with what:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema.graphql&lt;/span&gt; &lt;span class="c1"&gt;# full schema; could be HTTP link&lt;/span&gt;
&lt;span class="na"&gt;documents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;documents.graphql&lt;/span&gt; &lt;span class="c1"&gt;# queries and mutations that you want to have&lt;/span&gt;
&lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;./src/my-api/index.ts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# where to save&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;typescript&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;typescript-operations&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;typescript-react-apollo&lt;/span&gt; &lt;span class="c1"&gt;# stack that you are going to use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚠️ CAUTION
&lt;/h2&gt;

&lt;p&gt;🌿 &lt;a href="https://docs.lens.xyz/docs/introduction:" rel="noopener noreferrer"&gt;https://docs.lens.xyz/docs/introduction:&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This API is beta and not production complete yet, which means that we could change schemas and endpoints at any time without warning or notice to you. When this API is production ready, we will remove this beta warning and will endeavor to ensure that there are no changes going forward unless a major change to the protocol itself is required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Lens API is not production complete, and so is &lt;code&gt;@use-lens/*&lt;/code&gt;. Please, keep this in mind when going to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended use
&lt;/h3&gt;

&lt;p&gt;If you want to &lt;strong&gt;play with Lens API&lt;/strong&gt; - don't hesitate and install some of the &lt;code&gt;@use-lens/*&lt;/code&gt; packages - it will give you all you need to start.&lt;/p&gt;

&lt;p&gt;If you want to &lt;strong&gt;have more control&lt;/strong&gt; - use &lt;code&gt;@use-lens/cli&lt;/code&gt; to generate code locally. This would copy essential files that a package contains and would run &lt;code&gt;graphql-codegen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You would be able to do more with &lt;code&gt;codegen.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  👨‍🏫 React with Apollo Client
&lt;/h2&gt;

&lt;p&gt;For &lt;code&gt;@use-lens/react-apollo&lt;/code&gt;, I have prepared an example of how to use it.&lt;/p&gt;

&lt;p&gt;Check out the source code &lt;a href="https://github.com/use-lens/use-lens/tree/main/examples/react-apollo" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffhyf6utoa84gqjb66wzx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffhyf6utoa84gqjb66wzx.png" alt="use-lens-react-apollo-example.png" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25d9qluy1e9dc99b4o47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25d9qluy1e9dc99b4o47.png" alt="use-lens-react-apollo-example-2.png" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🤝 Lens API Documents
&lt;/h2&gt;

&lt;p&gt;The complete set of Lens API Documents has been taken from &lt;a href="https://github.com/lens-protocol/api-examples" rel="noopener noreferrer"&gt;api-examples&lt;/a&gt;, a repo of Lens Protocol that shows how to use Lens API.&lt;br&gt;
The same (or similar) queries are given as examples in Lens API docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 Summary
&lt;/h2&gt;

&lt;p&gt;If you would like to use Lens API to see what it is - simplify your developer experience by using &lt;a href="https://github.com/use-lens/use-lens" rel="noopener noreferrer"&gt;@use-lens&lt;/a&gt; or &lt;a href="https://the-guild.dev/graphql/codegen" rel="noopener noreferrer"&gt;GraphQL Code Generator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading! 🙇&lt;/p&gt;

</description>
      <category>web3</category>
      <category>graphql</category>
      <category>javascript</category>
      <category>lensprotocol</category>
    </item>
    <item>
      <title>Create Lens Subgraph on The Graph Protocol</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Fri, 26 Aug 2022 10:21:20 +0000</pubDate>
      <link>https://dev.to/andriishupta/create-lens-subgraph-on-the-graph-protocol-32f0</link>
      <guid>https://dev.to/andriishupta/create-lens-subgraph-on-the-graph-protocol-32f0</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/andriishupta/thegraph-hello-world" rel="noopener noreferrer"&gt;github: andriishupta/thegraph-hello-world&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thegraph.com/hosted-service/subgraph/andriishupta/hello-world" rel="noopener noreferrer"&gt;subgraph: andriishupta/hello-world&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thegraph.com/docs/en/developing/creating-a-subgraph/" rel="noopener noreferrer"&gt;TheGraph Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.lens.xyz/docs" rel="noopener noreferrer"&gt;Lens Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📰 Published on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.andriishupta.dev" rel="noopener noreferrer"&gt;Personal blog 💻&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andriishupta.medium.com" rel="noopener noreferrer"&gt;Medium ✍️&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ✨
&lt;/h2&gt;

&lt;p&gt;Initially, I got familiar with TheGraph by contributing to &lt;a href="https://twitter.com/developer_dao" rel="noopener noreferrer"&gt;DeveloperDAO&lt;/a&gt; to a project that did the same - indexing Lens Protocol.&lt;br&gt;
P.S. This tutorial is &lt;strong&gt;NOT&lt;/strong&gt; copy-paste of &lt;a href="https://github.com/Developer-DAO/Lens-Graph-Subgraph" rel="noopener noreferrer"&gt;existing code&lt;/a&gt;, and I created a repository from scratch to understand the basics of TheGraph and how to start. I needed to develop my subgraph to test how events would be indexed.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤓 Decentralized querying
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://thegraph.com/en/" rel="noopener noreferrer"&gt;The Graph&lt;/a&gt; is an indexing protocol for querying networks like Ethereum and IPFS. Anyone can build and publish open APIs, called subgraphs, making data easily accessible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Many different tools and APIs help us query blockchain data using centralized API like we used to with web2. My favourites are &lt;a href="https://www.alchemy.com/" rel="noopener noreferrer"&gt;alchemy&lt;/a&gt; and &lt;a href="https://infura.io/" rel="noopener noreferrer"&gt;infura&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But, if we want to go decentralized, we must use decentralized tools.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All data is stored and processed on open networks with verifiable integrity. TheGraph makes querying this data fast, reliable, and secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Lens
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://lens.xyz/" rel="noopener noreferrer"&gt;Lens Protocol&lt;/a&gt; is a composable and decentralized social graph, ready for you to build on so you can focus on creating a great experience, not scaling your users.&lt;/p&gt;

&lt;p&gt;Own your content.&lt;br&gt;
Own your social graph.&lt;br&gt;
Own your data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With &lt;a href="https://docs.lens.xyz/docs" rel="noopener noreferrer"&gt;Lens API&lt;/a&gt;, we could do everything we want with protocol, but it still includes the web2 principle and has a centralized database for different things built on top of Smart Contract data.&lt;/p&gt;

&lt;p&gt;If we want to get actual blockchain data, we need to use a protocol like TheGraph.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: assumption about the centralized part of Lens API is based on the features that Smart Contract has not, for example: "likes" functionality.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;I like Lens API very much and would count on it 99.9% of the time as a personal preference, balancing centralized vs. decentralized tooling&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  👀 How to create a subgraph
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;In this tutorial, I will give an example of creating a subgraph using &lt;a href="https://thegraph.com/hosted-service" rel="noopener noreferrer"&gt;Hosted Service&lt;/a&gt;&lt;/em&gt; - it will be closed in months. Still, there won't be a difference in coding approaches, just in how it is internally deployed. With the Hosted Service, it was easier to use for testing purposes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://thegraph.com/docs/en/deploying/hosted-service/#create-a-subgraph" rel="noopener noreferrer"&gt;graph init&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;install &lt;a href="https://github.com/graphprotocol/graph-cli" rel="noopener noreferrer"&gt;graph-cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;graph init --product hosted-service&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;follow CLI steps where you need to add protocol, name, and contract address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will get a generated project. The main entry point is &lt;strong&gt;subgraph.yaml&lt;/strong&gt;.&lt;br&gt;
This is the finished version of my subgraph.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/thegraph-hello-world/blob/main/subgraph.yaml" rel="noopener noreferrer"&gt;source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ewcsozcax6x41ombujy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ewcsozcax6x41ombujy.png" alt="lens-subgraph-yaml.png" width="800" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can read more on what is &lt;a href="https://thegraph.com/docs/en/developing/creating-a-subgraph/#the-subgraph-manifest" rel="noopener noreferrer"&gt;subgraph manifest&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  source block
&lt;/h4&gt;

&lt;p&gt;Indicates from what address to index. Many contracts use &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies" rel="noopener noreferrer"&gt;Proxy Upgrade Pattern&lt;/a&gt;, which helps to fix crucial bugs or update implementation. That is why I have added LensHub ABI(&lt;a href="https://www.alchemy.com/overviews/what-is-an-abi-of-a-smart-contract-examples-and-usage" rel="noopener noreferrer"&gt;Application Binary Interface&lt;/a&gt;) as source ABI and then changed the address to &lt;a href="https://docs.lens.xyz/docs/deployed-contract-addresses" rel="noopener noreferrer"&gt;proxy&lt;/a&gt;&lt;br&gt;
&lt;code&gt;startBlock&lt;/code&gt; - I chose some random block for testing purposes, so it won't start indexing from the start - it takes more time. Usually, this value is omitted or equals the Contract creation's block.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8bzuc2owtopb934tznc5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8bzuc2owtopb934tznc5.png" alt="image.png" width="740" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get ABI, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy from Etherscan/Polygonscan whole &lt;a href="https://polygonscan.com/address/0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d#code" rel="noopener noreferrer"&gt;Contract ABI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;a href="https://remix.ethereum.org/" rel="noopener noreferrer"&gt;remix.ethereum.org&lt;/a&gt;, copy the github project, and compile the contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I went with a 50/50 approach, such as Lens has &lt;a href="https://github.com/lens-protocol/lens-protocol/blob/main/contracts/libraries/Events.sol" rel="noopener noreferrer"&gt;&lt;code&gt;Events.sol&lt;/code&gt;&lt;/a&gt; library that is not compiled as part of the main contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;graph codegen&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After proper setup, we can run code generation - we will get all types of code to work with.&lt;/p&gt;

&lt;p&gt;✨ &lt;a href="https://thegraph.com/docs/en/developing/creating-a-subgraph/" rel="noopener noreferrer"&gt;The Graph&lt;/a&gt; has excellent documentation, so follow it and find all answers there. ✨ &lt;/p&gt;

&lt;h3&gt;
  
  
  Mindset shift
&lt;/h3&gt;

&lt;p&gt;As a developer who worked with databases, I started to think linearly: entity created -&amp;gt; entity updated. But events could be indexed from &lt;code&gt;startBlock&lt;/code&gt; and in non-linear order in time, so even if we index the event like &lt;code&gt;handleProfileImageURISet&lt;/code&gt;, we need to check if the entity existed previously(more in the code example).&lt;/p&gt;

&lt;p&gt;To find more tips, TheGraph documentation has a section &lt;a href="https://thegraph.com/docs/en/developing/creating-a-subgraph/#defining-entities" rel="noopener noreferrer"&gt;"defining entities"&lt;/a&gt; - it entirely describes what you should think about when creating your schema.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before defining entities, it is important to take a step back and think about how your data is structured and linked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;

&lt;p&gt;As we know - schema and how our data would be created is essential. Let's look at how I have added entity  definition:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/thegraph-hello-world/blob/main/schema.graphql" rel="noopener noreferrer"&gt;source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbtriwiyvwmblykzxo7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbtriwiyvwmblykzxo7e.png" alt="lens-subgraph-schema.png" width="800" height="870"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Profile&lt;/code&gt; is the &lt;code&gt;@entity&lt;/code&gt;, including default fields that I took from &lt;a href="https://docs.lens.xyz/docs/events#profilecreated" rel="noopener noreferrer"&gt;&lt;code&gt;ProfileCreated&lt;/code&gt;&lt;/a&gt; event and the &lt;code&gt;posts&lt;/code&gt; field, which is an &lt;a href="https://thegraph.com/docs/en/developing/creating-a-subgraph/#entity-relationships" rel="noopener noreferrer"&gt;entity relationship&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this case, it is a "One-To-Many" relationship with the usage of the &lt;a href="https://thegraph.com/docs/en/developing/creating-a-subgraph/#reverse-lookups" rel="noopener noreferrer"&gt;Reverse Lookup&lt;/a&gt; approach that TheGraph recommends us using.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For one-to-many relationships, the relationship should always be stored on the 'one' side, and the 'many' side should always be derived. ... will result in dramatically better performance for both indexing and querying the subgraph...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;Post&lt;/code&gt; is also an &lt;code&gt;@entity&lt;/code&gt;. Both have an &lt;code&gt;ID&lt;/code&gt;, which should be unique for the subgraph.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;lens-hub.ts&lt;/code&gt; are located functions that correspond to events we want to index.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;handle*Created&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;As we see, even for a new Post, we check if Profile already exists or not. I discovered the error I got during subgraph deployment and indexing that was saying something like "profile cannot be null" when I just wanted to load Profile to Post.&lt;/p&gt;

&lt;p&gt;TheGraph also supports the "merge" approach - this means that if we create a new instance of Profile and it already exists - it is okay cause the subgraph would try to merge fields. I wanted to be the more precise cause in the examples, and I saw that in every place where we wish to create something - we first check if it could be already created.&lt;/p&gt;

&lt;p&gt;Example: I have created a Post, and the subgraph knows that Post should have a Profile. Profile event was way before Post, so we don't have a Profile, and we need to create it; even if you could think - "how a Post could be created without a Profile" - it couldn't, but the event about the Post we could get first to index.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/thegraph-hello-world/blob/main/src/lens-hub.ts" rel="noopener noreferrer"&gt;source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fok7qiw5ck5c55ls0i0tw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fok7qiw5ck5c55ls0i0tw.png" alt="lens-subgraph-profile-created.png" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7w0m7yai4x8evqppila.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7w0m7yai4x8evqppila.png" alt="lens-subgraph-post-created.png" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ Testing
&lt;/h2&gt;

&lt;p&gt;To check out how it works, you could visit my &lt;a href="https://thegraph.com/hosted-service/subgraph/andriishupta/hello-world" rel="noopener noreferrer"&gt;subgraph: andriishupta/hello-world&lt;/a&gt;. It has a pre-defined "Test" query that will give you info for both Profiles and Posts. You could modify it to get more or less information in the query window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fizju8fanu5r7qyi2oubv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fizju8fanu5r7qyi2oubv.png" alt="lens-subgraph-example.png" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🙇
&lt;/h2&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>thegraph</category>
      <category>lens</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Generate Dummy Data in Strapi</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Tue, 16 Aug 2022 11:27:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/generate-dummy-data-in-strapi-3j1k</link>
      <guid>https://dev.to/andriishupta/generate-dummy-data-in-strapi-3j1k</guid>
      <description>&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;github: &lt;a href="https://github.com/andriishupta/strapi-generate-seed-data" rel="noopener noreferrer"&gt;andriishupta/strapi-generate-seed-data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Strapi's video with a general idea of how to &lt;a href="https://strapi.io/video-library/generate-dummy-data-in-strapi" rel="noopener noreferrer"&gt;"Generate dummy data"&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📰 Also published on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.andriishupta.dev" rel="noopener noreferrer"&gt;Personal Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andriishupta.medium.com" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤓 Motivation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://strapi.io/" rel="noopener noreferrer"&gt;Strapi&lt;/a&gt; is powerful open-source headless CMS that helps projects control code customization with extensibility and, at the same time, don't worry about implementing a full-blown Content Management System on their own.&lt;/p&gt;

&lt;p&gt;So, after you have set up your Strapi, you want to build Front-End on top of it. You hire front-end developers who are stuck: "Ye, I can query for data via REST and GraphQL, but what data should I see? Could I have an example?"&lt;/p&gt;

&lt;p&gt;😬&lt;/p&gt;

&lt;p&gt;To avoid this, we would generate example data upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌱 How to seed data?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Generate dummy data"
&lt;/h3&gt;

&lt;p&gt;There is the &lt;a href="https://strapi.io/video-library/generate-dummy-data-in-strapi" rel="noopener noreferrer"&gt;video&lt;/a&gt; in Strapi's Video-library that gave me a direction on how to do it.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;bootstrap&lt;/code&gt; &lt;a href="https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/functions.html#bootstrap" rel="noopener noreferrer"&gt;function&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;a href="https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/entity-service-api.html" rel="noopener noreferrer"&gt;Entity Service API&lt;/a&gt; for interactions&lt;/li&gt;
&lt;li&gt;generate dummy data with &lt;a href="https://fakerjs.dev/" rel="noopener noreferrer"&gt;@faker-js/faker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tip
&lt;/h3&gt;

&lt;p&gt;Your case 💯 would be more complicated, so don't forget that you could take almost everything from Strapi's &lt;a href="https://github.com/strapi/strapi" rel="noopener noreferrer"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;find a place on Strapi Admin you want to copy&lt;/li&gt;
&lt;li&gt;check URL and Network(in developer's inspection) to understand what is called&lt;/li&gt;
&lt;li&gt;find code(start with &lt;em&gt;controller&lt;/em&gt;) that corresponds to that calls&lt;/li&gt;
&lt;li&gt;enjoy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧑‍💻 Code
&lt;/h2&gt;

&lt;p&gt;We have a simple Todo application(duh) with a Todo collection and a Todo List as a page(single type) that we want to send to the front-end. Also, our Todos has media functionality, so we upload some.&lt;/p&gt;

&lt;p&gt;In the bootstrap function, we check for the development environment and decide if we should run seeding or not. Seeding would automatically run on the very first application run(valid when a developer clones an existing repository) and could be re-run with &lt;code&gt;yarn seed&lt;/code&gt; to force seed, which clears old and creates new data - &lt;code&gt;FORCE_APP_BOOTSTRAP_ONLY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;🔗&lt;a href="https://github.com/andriishupta/strapi-generate-seed-data/blob/main/src/index.ts#L19" rel="noopener noreferrer"&gt;source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febkmao5xc2hn3g1xn0wk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febkmao5xc2hn3g1xn0wk.png" alt="image.png" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Collection Type
&lt;/h3&gt;

&lt;p&gt;To create a todo using Entity Service API, we need to call the &lt;code&gt;create&lt;/code&gt; method with data that matches our entity. In the current example and during seeding, I have used "bulk promises" to run requests in parallel cause they are not dependent on each other.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/strapi-generate-seed-data/blob/main/src/_seed/todo.ts#L24" rel="noopener noreferrer"&gt;source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozcen857yx2g9wckx6hy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozcen857yx2g9wckx6hy.png" alt="image.png" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And using faker, we fill a todo like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphyu8zr86tsrbxh10jzs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphyu8zr86tsrbxh10jzs.png" alt="image.png" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Type
&lt;/h3&gt;

&lt;p&gt;Fulfilling the "Todo List page" is the same as Collection, but keep in mind that it could be only one entry all the time. Also, it contains a Todo relation, so we get five todos to fill it.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/strapi-generate-seed-data/blob/main/src/_seed/todo-list-page.ts#L14" rel="noopener noreferrer"&gt;source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd67hhspyyc20ha2a68f7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd67hhspyyc20ha2a68f7.png" alt="image.png" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Media Upload
&lt;/h3&gt;

&lt;p&gt;To attach media on a todo, we first need to upload that media and then link its id to the entity. Code has been copied from &lt;a href="https://github.com/strapi/strapi/blob/master/packages/core/upload/server/controllers/admin-upload.js#L57" rel="noopener noreferrer"&gt;Strapi's source code&lt;/a&gt;, modified, and I just created a helper function.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/strapi-generate-seed-data/blob/main/src/_seed/helpers.ts#L52" rel="noopener noreferrer"&gt;source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0pbxvpoabaf9o4g7tjw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0pbxvpoabaf9o4g7tjw.png" alt="image.png" width="800" height="651"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example in Todo:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qbikssku7kkjg4u3ivg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qbikssku7kkjg4u3ivg.png" alt="image.png" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ Results
&lt;/h2&gt;

&lt;p&gt;After opening the Admin panel, you will see generated data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flsnkczabka84m0d43h6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flsnkczabka84m0d43h6u.png" alt="image.png" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptbw7asl8s6mpprweewj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptbw7asl8s6mpprweewj.png" alt="image.png" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨
&lt;/h2&gt;

&lt;p&gt;Initially, I implemented this for &lt;a href="https://twitter.com/developer_dao" rel="noopener noreferrer"&gt;Developer DAO&lt;/a&gt;'s website &lt;a href="https://developerdao.com/" rel="noopener noreferrer"&gt;developerdao.com&lt;/a&gt;. Original code is located &lt;a href="https://github.com/Developer-DAO/cms" rel="noopener noreferrer"&gt;here&lt;/a&gt;(archived and moved to monorepo).&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>node</category>
      <category>strapi</category>
    </item>
    <item>
      <title>Setup Supabase with Nest.js</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Tue, 16 Aug 2022 05:35:50 +0000</pubDate>
      <link>https://dev.to/andriishupta/setup-supabase-with-nestjs-2kka</link>
      <guid>https://dev.to/andriishupta/setup-supabase-with-nestjs-2kka</guid>
      <description>&lt;h2&gt;
  
  
  ❗️⚠️ Disclaimer
&lt;/h2&gt;

&lt;p&gt;This article is created for Supabase &lt;strong&gt;v1&lt;/strong&gt; and seems not to work with &lt;strong&gt;v2&lt;/strong&gt; due to the depreciation of some &lt;strong&gt;auth&lt;/strong&gt; methods. I will create a new article for &lt;strong&gt;v2&lt;/strong&gt; and leave it here. If you have already found a solution — let me know in the comments! Thanks!&lt;/p&gt;

&lt;h2&gt;
  
  
  🤓 My expectations from You
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You know what the Supabase is&lt;/li&gt;
&lt;li&gt;You know the Nest.js framework&lt;/li&gt;
&lt;li&gt;You will google what is beyond this tutorial if needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤔 My use case for using Supabase in Nest.js
&lt;/h2&gt;

&lt;p&gt;I needed a polling mechanism that runs every 1 second and does some action.&lt;br&gt;
As Auth and DB, I have chosen to use Supabase cause I have seen some tutorials and wanted to try it out.&lt;/p&gt;

&lt;p&gt;At first, I wanted to use Next.js and Functions to do those operations by Cron Jobs, but it happens to be that the minimal time frame for Cron Job by Github(as the most accessible Cron provider) is only every 5 minutes.&lt;/p&gt;

&lt;p&gt;So I switched to the idea of a server(full) app with Nest.js. (want to deploy on &lt;a href="https://www.heroku.com/" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;
  
  
  🧑‍💻 How to use Supabase and Nest.js
&lt;/h2&gt;

&lt;p&gt;There is one single example of how to use Supabase as Auth library for your Nest.js application, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it didn't seem straightforward (actual implementation) to me, and I didn't want to use an external package&lt;/li&gt;
&lt;li&gt;it doesn't work as I wanted - it didn't have Supabase Client exposed from within the lib&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;it is still a good entry if you have never used Passport and need an example - it helped me to understand what to do in my code, so thanks **hiro1107&lt;/em&gt;* :)&lt;/p&gt;

&lt;p&gt;📑 examples: &lt;a href="https://supabase.com/docs/guides/examples" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🧑‍💻 Github: &lt;a href="https://github.com/hiro1107/nestjs-supabase-auth" rel="noopener noreferrer"&gt;https://github.com/hiro1107/nestjs-supabase-auth&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;So I have decided to implement my way for Auth and Client.&lt;/p&gt;
&lt;h3&gt;
  
  
  Could we use Supabase on the Node.js back-end?
&lt;/h3&gt;

&lt;p&gt;In general, like Firebase, Supabase is a client-side tool that works from the client-side by providing the "anon" user with Row Level Security Policies, but it doesn't force us to use Supabase only on the client-side.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;With some rules, we can freely leverage the power of Supabase in our Node.js Server.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🍴 Supabase basic setup
&lt;/h2&gt;

&lt;p&gt;There are a few key points that I want to mention before the code.&lt;/p&gt;
&lt;h3&gt;
  
  
  RLS Policies
&lt;/h3&gt;

&lt;p&gt;Enable it when you create tables&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuv265l8iwbifwm37h5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuv265l8iwbifwm37h5s.png" alt="Image description" width="800" height="452"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;I recommend creating two by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"anon" is rejected by default for ALL actions&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiri9ntimdx7t6pk8kuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiri9ntimdx7t6pk8kuw.png" alt="Image description" width="800" height="421"&gt;&lt;/a&gt; &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ALL actions are only could be done by auth.uid(), if your app is "user-oriented"&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrfqiiz7hu313a9o899k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrfqiiz7hu313a9o899k.png" alt="Image description" width="800" height="382"&gt;&lt;/a&gt; &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;user_id&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To make your application work with previous policies, don't forget to create the &lt;code&gt;user_id&lt;/code&gt; column on each table that you use: it should be NOT null auto-generated &lt;code&gt;auth.uid()&lt;/code&gt; field, so in this way, Supabase will always append the correct user to a row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn06ymyr8fygqq56amdxi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn06ymyr8fygqq56amdxi.png" alt="Image description" width="800" height="385"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;You can bypass RLS with Service Keys if needed, but be careful.&lt;/p&gt;
&lt;h2&gt;
  
  
  🐱 Nest.js application
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Installs
&lt;/h3&gt;

&lt;p&gt;To make this work, we need to add a few libs in addition to Supabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i passport passport-jwt @nestjs/passport @supabase/supabase-js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;passport - handles everything related to Auth, does it magic that we don't need to care about&lt;/p&gt;

&lt;p&gt;passport-jwt - has a ready-to-use Strategy for JWT Auth&lt;/p&gt;

&lt;p&gt;@nestjs/passport - Nest.js's module for Passport&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;How JWT Auth works with Passport&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;How JWT Auth works go beyond this article, but there are plenty of explanations for using Passport and passport-jwt in Nest.js. You can start by checking Nest.js's &lt;a href="https://docs.nestjs.com/security/authentication" rel="noopener noreferrer"&gt;Authentication&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What we need to know for our case: Supabase is JWT-based authorization, which handles everything on its side. On our side, we need to have the correct &lt;code&gt;SUPABASE_JWT_SECRET&lt;/code&gt;, which is used to decode JWTs. (located in Settings -&amp;gt; API)&lt;/p&gt;

&lt;h3&gt;
  
  
  Supabase folder
&lt;/h3&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/nestjs-supabase-setup/tree/main/src/common/supabase" rel="noopener noreferrer"&gt;Link&lt;/a&gt; to folder&lt;/p&gt;

&lt;p&gt;This is the main code you could copy to your code base, and it will just work. The module is a regular Nest.js module. Other files deserve a deeper look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Passport has a rich ecosystem of strategies that implement various authentication mechanisms. While simple in concept, the set of Passport strategies you can choose from is large and presents a lot of variety&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Such as Supabase is JWT, we will use a ready-to-use passport-jwt strategy that does all decoding and other things for us.&lt;/p&gt;

&lt;p&gt;In this code, we extend PassportStrategy with JWT Strategy and pass config in the &lt;code&gt;super&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;strange "extend" &lt;code&gt;PassportStrategy(Strategy)&lt;/code&gt; is TypeScript Mixins&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/nestjs-supabase-setup/blob/main/src/common/supabase/supabase.strategy.ts" rel="noopener noreferrer"&gt;source-code&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgen7rhtlerdrsp0cin94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgen7rhtlerdrsp0cin94.png" alt="supabase strategy" width="800" height="675"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  Guard
&lt;/h3&gt;

&lt;p&gt;With this guard, I have protected the whole application by providing &lt;code&gt;APP_GUARD&lt;/code&gt; in app.module.ts - a global way of guarding. You could use &lt;code&gt;UseGuard&lt;/code&gt; for routes that need to be protected.&lt;/p&gt;

&lt;p&gt;Here we extend AuthGuard with the &lt;code&gt;jwt&lt;/code&gt; strategy(this is how passport-jwt named Strategy under the hood). Easy.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/nestjs-supabase-setup/blob/main/src/common/supabase/supabase.guard.ts" rel="noopener noreferrer"&gt;source-code&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxkmzxfioqwdkck8hhwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxkmzxfioqwdkck8hhwu.png" alt="supabase guard" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Service: &lt;code&gt;Scope.REQUEST&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This service will &lt;code&gt;createClient&lt;/code&gt; for every request and &lt;code&gt;setAuth&lt;/code&gt;, so we will have the correct user during all following service calls. Code implemented in "Singleton" manner so we will get the same instance in different places during the same request.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/andriishupta/nestjs-supabase-setup/blob/main/src/common/supabase/supabase.ts" rel="noopener noreferrer"&gt;source-code&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv5khj98j9k5st2wkcf5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv5khj98j9k5st2wkcf5.png" alt="supabase-service" width="800" height="1013"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Here comes an interesting part that happens with different libraries that hold client-side states:&lt;/p&gt;

&lt;p&gt;If we use client-side libs that hold state per user, on server OR SSR apps like Next.js, we need to be careful with those, such as it could be that when we create an instance of a library, it could become available for everyone who calls server/SSR apps.&lt;/p&gt;

&lt;p&gt;That is why it is essential to use &lt;code&gt;@Injectable({ scope: Scope.REQUEST })&lt;/code&gt; so our Supabase &lt;code&gt;createClient&lt;/code&gt; will be created per request, and we will set auth correctly.&lt;/p&gt;

&lt;p&gt;Another example: &lt;a href="https://react-query-v3.tanstack.com/guides/ssr#using-hydration" rel="noopener noreferrer"&gt;Next.js &amp;amp; SSR with react-query&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkddcmvqe0w85q1ok20x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkddcmvqe0w85q1ok20x.png" alt="Image description" width="800" height="317"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I haven't tested it for multiple users yet, but I am pretty sure about it.&lt;/em&gt;&lt;br&gt;
I haven't found other ways to pass Auth with one Client in docs by appending &lt;code&gt;access_token&lt;/code&gt; - only &lt;code&gt;setAuth&lt;/code&gt; to Client directly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Possible solution&lt;/em&gt;: Supabase could tweak auth flow, where with a unique setup, we can pass users in each request. With the Passport, we have the user in &lt;code&gt;req.user&lt;/code&gt; and can still access the Authorization header for &lt;code&gt;access_token&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  📭 Testing with Postman
&lt;/h2&gt;

&lt;p&gt;After everything is finished - let's try to call our server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwy9tx9un12ge6mfa8sy7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwy9tx9un12ge6mfa8sy7.png" alt="Image description" width="800" height="541"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;As expected, we get a 401 code - this is &lt;em&gt;passport.js&lt;/em&gt; does it check for JWT in our &lt;strong&gt;SupabaseGuard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's now get a valid &lt;code&gt;access_token&lt;/code&gt; and repeat the call:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokrxs9c735mvnphynn3s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokrxs9c735mvnphynn3s.png" alt="Image description" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💪 💪 💪&lt;/p&gt;

&lt;p&gt;&lt;em&gt;access token available after you log in with a user. In my case, I have just sent "Magic Link" and got &lt;code&gt;acces_token&lt;/code&gt; from the URL&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkxbh38wzpda28xy3fnh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkxbh38wzpda28xy3fnh.png" alt="Image description" width="800" height="170"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  📋 Summary - TL;DR;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;install required dependencies&lt;/li&gt;
&lt;li&gt;copy &lt;strong&gt;supabase&lt;/strong&gt; folder to your project&lt;/li&gt;
&lt;li&gt;add 3 SUPABASE_* variables to .env&lt;/li&gt;
&lt;li&gt;import &lt;em&gt;supabase.module&lt;/em&gt;: in app.module / &lt;a class="mentioned-user" href="https://dev.to/global"&gt;@global&lt;/a&gt;() auth.module / etc.&lt;/li&gt;
&lt;li&gt;provide global &lt;code&gt;APP_GUARD&lt;/code&gt; or use it where you need it with &lt;code&gt;UseGuard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;em&gt;supabase.ts&lt;/em&gt; in other services by &lt;code&gt;this.supabase.getClient()&lt;/code&gt; for Supabase calls&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔗 Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Github: &lt;a href="https://github.com/andriishupta/nestjs-supabase-setup" rel="noopener noreferrer"&gt;https://github.com/andriishupta/nestjs-supabase-setup&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nest.js's &lt;a href="https://docs.nestjs.com/security/authentication" rel="noopener noreferrer"&gt;Authentication&lt;/a&gt;, including JWT with Passport.js&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to use &lt;a href="https://docs.nestjs.com/guards" rel="noopener noreferrer"&gt;Guards&lt;/a&gt; in Nest.js&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Supabase's Auth &lt;a href="https://github.com/hiro1107/nestjs-supabase-auth" rel="noopener noreferrer"&gt;example app with Nest.js&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://supabase.com/docs/reference" rel="noopener noreferrer"&gt;Supabase's docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Link to &lt;a href="https://blog.devgenius.io/setup-supabase-with-nest-js-85041b03ec3a" rel="noopener noreferrer"&gt;Medium's copy of the article&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>nestjs</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Cross-Origin iframe communication with Window.postMessage</title>
      <dc:creator>Andrii Shupta</dc:creator>
      <pubDate>Tue, 16 Aug 2022 05:19:00 +0000</pubDate>
      <link>https://dev.to/andriishupta/cross-origin-iframe-communication-with-windowpostmessage-31pp</link>
      <guid>https://dev.to/andriishupta/cross-origin-iframe-communication-with-windowpostmessage-31pp</guid>
      <description>&lt;h2&gt;
  
  
  🤔 Why do we need cross-origin iframe communication?
&lt;/h2&gt;

&lt;p&gt;Imagine that you need to integrate with the "3rd party service" that would be used as part of your application.&lt;/p&gt;

&lt;p&gt;Both of your companies are just start-ups, and we don't have a complete variety of tools that will make our lives easier, so we choose &lt;code&gt;iframe&lt;/code&gt; as the first option. We &lt;em&gt;must&lt;/em&gt; integrate what we have now for the beta version. After that, we will refactor the code and will use edge technologies, as our manager promised(😉)&lt;/p&gt;

&lt;p&gt;Their app(as an example) could show private information, possibly, some real-time bank details / shipping / trading details, and only available after user authorization.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤓 What could be a better solution?
&lt;/h3&gt;

&lt;p&gt;The best version of integration(IMHO) would be to get a react library with components, hooks, utils, etc., that will do everything for us. For example, check out &lt;a href="https://stripe.com/docs/stripe-js/react" rel="noopener noreferrer"&gt;React Stripe.js Components&lt;/a&gt;. Second best - take an Open API(example &lt;a href="https://stripe.com/docs/api" rel="noopener noreferrer"&gt;Stripe API&lt;/a&gt;) and implement our own components.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤨 What are we going to build?
&lt;/h2&gt;

&lt;h4&gt;
  
  
  💭 Idea summary
&lt;/h4&gt;

&lt;p&gt;As the Parent app, we want to login within the &lt;code&gt;iframe&lt;/code&gt; with some token, so the &lt;code&gt;iframe&lt;/code&gt; could show relative information. Every N mins(5 secs in this case), our token would expire, and the &lt;code&gt;iframe&lt;/code&gt; needs to request another. As a bonus, we can change a theme from &lt;em&gt;dark&lt;/em&gt; to &lt;em&gt;light&lt;/em&gt;, which could happen from both sides.&lt;/p&gt;




&lt;p&gt;Mostly I would list code that is only related to the &lt;code&gt;iframe&lt;/code&gt; and &lt;code&gt;Web API&lt;/code&gt; part and won't focus on things like an app creation or an explanation of &lt;a href="https://nextjs.org/docs/deployment#managed-nextjs-with-vercel" rel="noopener noreferrer"&gt;&lt;em&gt;"how to deploy to Vercel"&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Parent and the Child apps would be our actual implementation of what we need. For the front-end, we are going to use &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; and &lt;a href="https://chakra-ui.com/" rel="noopener noreferrer"&gt;Chakra-UI&lt;/a&gt; for components. We would deploy apps on &lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt; and &lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt;(to be truly cross-origin).&lt;/p&gt;

&lt;p&gt;Also, I would use &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;Nrwl Nx's workspaces&lt;/a&gt; to have monorepo, keeping run/build processes seamless.&lt;/p&gt;

&lt;h2&gt;
  
  
  👨‍💻 Code(skip to this if you don't want to read the Intro)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🤖 "The Communicator."
&lt;/h3&gt;

&lt;p&gt;🔗 &lt;a href="https://iframe-communicator.vercel.app" rel="noopener noreferrer"&gt;https://iframe-communicator.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 Github: &lt;a href="https://github.com/andriishupta/iframe-communicator" rel="noopener noreferrer"&gt;https://github.com/andriishupta/iframe-communicator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is a "special" app you could use for &lt;strong&gt;&lt;em&gt;real-world testing&lt;/em&gt;&lt;/strong&gt; to see how messaging works in your app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvv0ta4ag6ay2bg0xrmv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvv0ta4ag6ay2bg0xrmv3.png" alt="iframe-communicator.png" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧑 Parent code
&lt;/h3&gt;

&lt;p&gt;🔗 &lt;a href="https://cross-origin-iframe-communication-with-nextjs-parent-app.vercel.app/" rel="noopener noreferrer"&gt;link&lt;/a&gt; to the deployed app&lt;/p&gt;

&lt;p&gt;🔗 source code is available for copying &lt;a href="https://github.com/andriishupta/cross-origin-iframe-communication-with-nextjs/blob/main/packages/parent-app/pages/index.tsx" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As for the Parent app, we will surely have &lt;code&gt;iframe&lt;/code&gt; rendered on our side. Let's start from it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fboijveg8ftfif3d10nmn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fboijveg8ftfif3d10nmn.png" alt="iframe3.png" width="800" height="935"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iframeRef&lt;/strong&gt; is our &lt;a href="https://reactjs.org/docs/hooks-reference.html#useref" rel="noopener noreferrer"&gt;React.js reference&lt;/a&gt; to the DOM element, so we can later use it&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;onLoad&lt;/em&gt; - this would send my initial token&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Next: how we send the message is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage" rel="noopener noreferrer"&gt;Window.postMessage&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7auz1mn8vcmfjk9rdq9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7auz1mn8vcmfjk9rdq9d.png" alt="post-message.png" width="800" height="683"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;iframeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CHILD_APP_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// OR use '*' to handle all origins&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;postMessage&lt;/code&gt; takes a &lt;code&gt;message: Message&lt;/code&gt; as the argument - it is our own message &lt;strong&gt;kind&lt;/strong&gt; that we selected and agreed with the Child app to pass through:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The data is serialized using the structured clone algorithm. This means you can pass a broad variety of data objects safely to the destination window without having to serialize them yourself.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;To send actual message we are using &lt;code&gt;iframeRef.current.contentWindow&lt;/code&gt; as our &lt;code&gt;targetWindow&lt;/code&gt;(from documentation) and the function's second parameter is &lt;code&gt;targetOrigin&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Specifies what the origin of targetWindow must be for the event to be dispatched, either as the literal string "*"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I know my &lt;code&gt;targetOrigin&lt;/code&gt;, so I am passing it and suggesting you not neglect &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns" rel="noopener noreferrer"&gt;security risks&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Last but not least, we want to listen to messages from the Child!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26r5ac4d89rqadmwmzmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26r5ac4d89rqadmwmzmu.png" alt="listener.png" width="800" height="885"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Security and filtering: we accept only our messages that we are sure in&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// skip other messages for security reasons and avoid extensions alerts in console
if (event.origin !== CHILD_APP_URL) {
  return;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's get the data from the &lt;em&gt;MessageEvent&lt;/em&gt; and do some checks and act by business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (message?.type === 'token-expired-from-child') {
  ...
} else if (message?.type === 'theme-from-child') {
  ...
} else {
  //  in case of some random message
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;for more options this code could be improved with switch/case(who likes), ternary operator, or object literals.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finish up by adding a listener and return a callback for removal, so when a component goes down, you navigate to another page, where you don't need to listen for an &lt;code&gt;iframe&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;window.addEventListener('message', handler);
return () =&amp;gt; window.removeEventListener('message', handler);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  👶 Child code
&lt;/h3&gt;

&lt;p&gt;🔗 &lt;a href="https://lustrous-donut-e3b29a.netlify.app" rel="noopener noreferrer"&gt;link&lt;/a&gt; to the deployed app&lt;/p&gt;

&lt;p&gt;🔗 source code is available for copying &lt;a href="https://github.com/andriishupta/cross-origin-iframe-communication-with-nextjs/blob/main/packages/child-app/pages/index.tsx" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The approach is the same for the Child app, with a twist of where to call &lt;em&gt;postMessage&lt;/em&gt; - &lt;code&gt;window.parent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3wlmmok208hhlzacd0t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3wlmmok208hhlzacd0t.png" alt="child-post-message.png" width="800" height="886"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;And listening to the messages differs in &lt;code&gt;type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kgkkxkm566mtv3z4zbr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kgkkxkm566mtv3z4zbr.png" alt="child-listener.png" width="800" height="822"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;p&gt;🎨 Parent app: &lt;a href="https://cross-origin-iframe-communication-with-nextjs-parent-app.vercel.app" rel="noopener noreferrer"&gt;https://cross-origin-iframe-communication-with-nextjs-parent-app.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👨‍💻 Github: &lt;a href="https://github.com/andriishupta/cross-origin-iframe-communication-with-nextjs" rel="noopener noreferrer"&gt;https://github.com/andriishupta/cross-origin-iframe-communication-with-nextjs&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🤖 "The Communicator": &lt;a href="https://iframe-communicator.vercel.app" rel="noopener noreferrer"&gt;https://iframe-communicator.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👨‍💻 Github for "The Communicator": &lt;a href="https://github.com/andriishupta/cross-origin-iframe-communication-with-nextjs" rel="noopener noreferrer"&gt;https://github.com/andriishupta/cross-origin-iframe-communication-with-nextjs&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📝 Summary
&lt;/h2&gt;

&lt;p&gt;Cross-Origin iframe communication could come in quite handy in specific situations, and we totally could take advantage of two-way messaging to make that even more dynamic. Check for yourself by clicking the examples.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cors</category>
      <category>react</category>
      <category>iframe</category>
    </item>
  </channel>
</rss>
