<?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: Wes E</title>
    <description>The latest articles on DEV Community by Wes E (@wes_e_fa9b9483d1243c16761).</description>
    <link>https://dev.to/wes_e_fa9b9483d1243c16761</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%2F3461567%2Fa607dab7-548f-442d-a6eb-96ae72e2f1a3.png</url>
      <title>DEV Community: Wes E</title>
      <link>https://dev.to/wes_e_fa9b9483d1243c16761</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wes_e_fa9b9483d1243c16761"/>
    <language>en</language>
    <item>
      <title>I built a native DuckDB IDE in Rust, and the client is DuckDB too</title>
      <dc:creator>Wes E</dc:creator>
      <pubDate>Wed, 30 Sep 2026 15:24:23 +0000</pubDate>
      <link>https://dev.to/wes_e_fa9b9483d1243c16761/i-built-a-native-duckdb-ide-in-rust-and-the-client-is-duckdb-too-fhg</link>
      <guid>https://dev.to/wes_e_fa9b9483d1243c16761/i-built-a-native-duckdb-ide-in-rust-and-the-client-is-duckdb-too-fhg</guid>
      <description>&lt;p&gt;Most SQL GUIs treat DuckDB as one more driver on a list. You get the SQL every&lt;br&gt;
database shares, and DuckDB's best parts — FROM-first queries, DESCRIBE, table&lt;br&gt;
functions, its own types and admin functions — come second.&lt;/p&gt;

&lt;p&gt;I wanted a client that only speaks DuckDB, so I built one. DuckPlus is a free,&lt;br&gt;
MIT-licensed Mac app written in Rust on gpui, the UI framework Zed is built with.&lt;br&gt;
This post covers the parts that were interesting to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client is DuckDB too
&lt;/h2&gt;

&lt;p&gt;DuckDB now has its own client/server protocol, Quack. A server takes one line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CALL quack_serve('quack:localhost', token := 'super_secret');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;DuckPlus embeds an in-memory DuckDB whose only job is to speak Quack. When you&lt;br&gt;
press Cmd+Enter, this is what goes over the wire:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM quack_query(
  'quack:db.internal:9494',
  '&amp;lt;your sql&amp;gt;',
  token := '...'
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The server's own parser handles your statement, DDL and multi-statement scripts&lt;br&gt;
included. Results come back in DuckDB's vector format, not through a generic&lt;br&gt;
driver. I use quack_query instead of ATTACH 'quack:...' because attaching still&lt;br&gt;
has catalog gaps.&lt;/p&gt;

&lt;p&gt;Local .duckdb files are opened by the same embedded engine, so a file and a&lt;br&gt;
server behave the same way in the app.&lt;/p&gt;

&lt;p&gt;Three more choices keep the UI responsive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema lookups run on their own connection, so the tree never waits behind a
long query.&lt;/li&gt;
&lt;li&gt;Every query runs on a fresh clone of the connection, so a cancelled one can't
block the next.&lt;/li&gt;
&lt;li&gt;Each new query window gets its own connections to the same server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A cell becomes text only when it's on screen
&lt;/h2&gt;

&lt;p&gt;Results stay as the Arrow batches DuckDB returned. The grid is virtualized: it&lt;br&gt;
builds only the rows you can see, and formats only the cells in them. Rows you&lt;br&gt;
never scroll to are never turned into strings.&lt;/p&gt;

&lt;p&gt;The grid stops pulling batches at 10,000 rows by default, and the status bar&lt;br&gt;
tells you when it hits the limit. Sorting knows what it has: if every row is&lt;br&gt;
loaded it sorts in place, and if not, it asks the server again with ORDER BY.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editing without surprises
&lt;/h2&gt;

&lt;p&gt;You can double-click a cell and type. Edits are staged until Cmd+S, and then&lt;br&gt;
they all go in one transaction. The hard part is making sure an UPDATE changes&lt;br&gt;
exactly the row you meant. So every UPDATE is paired with a check:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN TRANSACTION;
SELECT CASE WHEN count(*) &amp;lt;&amp;gt; 1
  THEN error('Expected 1 row where "id" = ...') END
FROM "analytics"."main"."users" WHERE "id" = '10437';
UPDATE "analytics"."main"."users"
SET "plan" = 'team', "mrr" = '49'
WHERE "id" = '10437';
COMMIT;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If the key isn't really unique, or someone deleted the row in the meantime, the&lt;br&gt;
check raises an error inside the transaction and nothing is saved.&lt;/p&gt;

&lt;p&gt;Rows are found by the primary key, then a UNIQUE constraint, then a column you&lt;br&gt;
pick. DuckLake tables declare no keys, so there you pick one. A plain&lt;br&gt;
SELECT ... FROM one_table is editable; joins, GROUP BY and CTEs make the result&lt;br&gt;
read-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small guardrails
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DROP, DELETE and TRUNCATE need a second Cmd+Enter. The check matches whole
words, so a column called dropped_at doesn't trigger it.&lt;/li&gt;
&lt;li&gt;Every saved connection has a colour, shown in the title bar. Red means
production.&lt;/li&gt;
&lt;li&gt;Tokens go to the macOS Keychain, not to a file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Admin screens are just queries
&lt;/h2&gt;

&lt;p&gt;The sidebar has seven server views: databases, storage, extensions, settings,&lt;br&gt;
memory, secrets and running Quack servers. Clicking one puts its SQL in the&lt;br&gt;
editor and runs it. For example, Extensions is:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT extension_name, loaded, installed, extension_version, install_mode, description
FROM duckdb_extensions()
ORDER BY loaded DESC, installed DESC, extension_name;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can see it, change it and keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do (yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Quack is beta until DuckDB 2.0 and has no remote cancel. Cmd+. frees the
window, but the server finishes the statement.&lt;/li&gt;
&lt;li&gt;It's DuckDB only, on purpose.&lt;/li&gt;
&lt;li&gt;It's Mac only for now (macOS 12+, Apple silicon and Intel). gpui runs on
Windows and Linux, but the installer and bundle don't yet.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Download: &lt;a href="https://duckhouse.co/duckplus" rel="noopener noreferrer"&gt;https://duckhouse.co/duckplus&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/wes/duckplus" rel="noopener noreferrer"&gt;https://github.com/wes/duckplus&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a hosted server to point it at, every DuckHouse database is a Quack&lt;br&gt;
server. That's what I work on day to day. But DuckPlus works with any&lt;br&gt;
quack_serve, including one on your laptop.&lt;/p&gt;

&lt;p&gt;Issues and PRs are welcome. I'd especially like to hear from people using&lt;br&gt;
DuckLake or very large local files.&lt;/p&gt;

</description>
      <category>ide</category>
      <category>duckdb</category>
      <category>database</category>
      <category>rust</category>
    </item>
    <item>
      <title>Your contract templates should live in your repo, not in a WYSIWYG editor</title>
      <dc:creator>Wes E</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:09:19 +0000</pubDate>
      <link>https://dev.to/wes_e_fa9b9483d1243c16761/your-contract-templates-should-live-in-your-repo-not-in-a-wysiwyg-editor-10mh</link>
      <guid>https://dev.to/wes_e_fa9b9483d1243c16761/your-contract-templates-should-live-in-your-repo-not-in-a-wysiwyg-editor-10mh</guid>
      <description>&lt;p&gt;Every time I've needed signatures inside a product I was building, the same thing&lt;br&gt;
happened. The signature part took an afternoon. The &lt;em&gt;contract&lt;/em&gt; part took weeks —&lt;br&gt;
because the legal text lived in someone else's WYSIWYG editor, behind a login,&lt;br&gt;
with no version history anybody on my team could review.&lt;/p&gt;

&lt;p&gt;That's a strange place to keep the one document with legal consequences.&lt;/p&gt;

&lt;p&gt;So the tool I ended up building keeps templates as Markdown files. Here is the&lt;br&gt;
whole format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Freelance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Agreement"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Flexible&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agreement&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;freelance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;work&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;projects."&lt;/span&gt;
&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Services&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# FREELANCE AGREEMENT&lt;/span&gt;

&lt;span class="gs"&gt;**Effective Date:**&lt;/span&gt; {{effective_date}}

This Freelance Agreement is entered into by:

&lt;span class="gs"&gt;**Freelancer:**&lt;/span&gt;
{{freelancer_name}}
{{freelancer_address}}
{{freelancer_email}}

&lt;span class="gu"&gt;## 1. PROJECT DESCRIPTION&lt;/span&gt;

{{project_description}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontmatter, headings, &lt;code&gt;{{variables}}&lt;/code&gt;. That's it. There is no other syntax to&lt;br&gt;
learn, because there is no other syntax.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why this shape matters more than it sounds like it should
&lt;/h3&gt;

&lt;p&gt;A Markdown template is a file. Files do things that editor content can't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It diffs.&lt;/strong&gt; When someone changes the indemnity clause, that change shows up in&lt;br&gt;
a pull request as a red line and a green line. A non-engineer can read that diff&lt;br&gt;
and tell you what changed. Try getting that out of a rich-text editor's revision&lt;br&gt;
history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It reviews.&lt;/strong&gt; The contract your company sends is now subject to the same&lt;br&gt;
approval process as the code your company ships. Same reviewers, same audit&lt;br&gt;
trail, same blame. If your legal counsel wants to own the clause language, they&lt;br&gt;
own a file — and &lt;code&gt;git log&lt;/code&gt; says when they last touched it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It tests.&lt;/strong&gt; You can lint it. You can grep every template for a term you're&lt;br&gt;
retiring. You can write a test that fails if a template loses a required&lt;br&gt;
variable. I have all of these; none of them were possible when the source of&lt;br&gt;
truth was a form field on someone else's server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's yours.&lt;/strong&gt; The template is in your repo whether or not you keep using my&lt;br&gt;
product. That's a deliberate property, not an accident — I've been on the wrong&lt;br&gt;
end of an export button before.&lt;/p&gt;
&lt;h3&gt;
  
  
  The full flow, end to end
&lt;/h3&gt;

&lt;p&gt;Four calls. Auth is an &lt;code&gt;x-api-key&lt;/code&gt; header on all of them; the base is&lt;br&gt;
&lt;code&gt;https://apisign.io/api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Push a template.&lt;/strong&gt; &lt;code&gt;content&lt;/code&gt; is the Markdown above, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://apisign.io/api/template/create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$APISIGN_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "Freelance Agreement",
    "content": "# FREELANCE AGREEMENT\n\n**Effective Date:** {{effective_date}}\n..."
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns &lt;code&gt;{ "template": { "id": "...", ... } }&lt;/code&gt;. Every save writes a version row,&lt;br&gt;
so the server keeps its own history alongside your git history.&lt;/p&gt;

&lt;p&gt;The natural next step is a CI job: on merge to &lt;code&gt;main&lt;/code&gt;, POST each changed&lt;br&gt;
&lt;code&gt;.md&lt;/code&gt; file. Your templates are then deployed the same way your code is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Create a contract&lt;/strong&gt; by filling the variables in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://apisign.io/api/contract/create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$APISIGN_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "Freelance Agreement — Jane Doe",
    "template_id": "TEMPLATE_ID",
    "variables": {
      "effective_date": "2026-08-10",
      "freelancer_name": "Jane Doe",
      "freelancer_email": "jane@example.com",
      "project_description": "Design system refresh, 6 weeks."
    },
    "expires_in_days": 14,
    "signers": [
      { "email": "jane@example.com", "name": "Jane Doe", "signing_order": 1 }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back the contract and its signers. Each signer carries an explicit&lt;br&gt;
&lt;code&gt;signing_order&lt;/code&gt;, so sequential signing is a number rather than a workflow&lt;br&gt;
builder. The signing link itself is generated at send time and goes into the&lt;br&gt;
email — it is not on the create response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Send it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://apisign.io/api/contract/send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$APISIGN_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "contract_id": "CONTRACT_ID" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signers get an email with their link. They sign in the browser — no account, no&lt;br&gt;
app, no download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Find out when they signed.&lt;/strong&gt; Register a webhook endpoint and you'll get&lt;br&gt;
&lt;code&gt;contract_signed&lt;/code&gt; when a signer finishes and &lt;code&gt;contract_completed&lt;/code&gt; when everyone&lt;br&gt;
has. (Yes, underscores. Stripe and GitHub use dots and I'd probably pick dots if&lt;br&gt;
I were starting today, but renaming them is a migration with a live integration&lt;br&gt;
on the other end, not a Tuesday.)&lt;/p&gt;

&lt;p&gt;One thing I'd tell you to do anyway: &lt;strong&gt;reconcile against &lt;code&gt;GET /contract/get&lt;/code&gt;&lt;br&gt;
rather than trusting delivery.&lt;/strong&gt; Webhooks retry with backoff, but the retry is&lt;br&gt;
kicked by your org's next event rather than by a scheduler, so a failed delivery&lt;br&gt;
to a quiet account waits. Every attempt is visible via the webhook API. I'd&lt;br&gt;
rather you know that than discover it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it costs, since that's the next question
&lt;/h3&gt;

&lt;p&gt;$0.25 per contract sent. No subscription, no per-seat charge, no minimum. A&lt;br&gt;
contract with six signers costs the same as one with one signer, because the&lt;br&gt;
charge is per send, not per signature. A resend goes through the same path, so it&lt;br&gt;
costs another $0.25 — there's no free reminder, and I'd rather say that here than&lt;br&gt;
have you find it on the invoice.&lt;/p&gt;

&lt;p&gt;Sign up, create an organization, and you have $5.00 on it — 20 contracts, no&lt;br&gt;
card.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it falls short
&lt;/h3&gt;

&lt;p&gt;Nobody outside my own companies has paid for this yet. That's the honest number&lt;br&gt;
and I'd rather lead with it than build a logo wall.&lt;/p&gt;

&lt;p&gt;Some specifics, because "early" is a word people use to avoid a list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API key &lt;code&gt;read&lt;/code&gt; / &lt;code&gt;read_write&lt;/code&gt; permissions aren't enforced yet.&lt;/strong&gt; The setting
is stored and displayed; nothing checks it. Treat every key as read-write and
don't hand one to a third party expecting it to be scoped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;There's no rate limiting on the API.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No idempotency key on send.&lt;/strong&gt; If your HTTP client retries a timed-out send,
you can be charged twice and your signer gets two emails. Guard the call on
your side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook retries are best-effort&lt;/strong&gt;, per above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are 37 templates in the library to start from, and an MCP server if you&lt;br&gt;
want an agent driving it.&lt;/p&gt;

&lt;p&gt;If you keep contract text in a WYSIWYG editor today and it's working fine for&lt;br&gt;
you, this isn't worth switching for. If you've ever tried to find out who changed&lt;br&gt;
a clause and when, I built this for that.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://apisign.io/docs" rel="noopener noreferrer"&gt;https://apisign.io/docs&lt;/a&gt; — happy to answer anything here, including the&lt;br&gt;
parts that don't work.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
