<?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: Setasena Randata</title>
    <description>The latest articles on DEV Community by Setasena Randata (@setasena_randata_1cfa30f4).</description>
    <link>https://dev.to/setasena_randata_1cfa30f4</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3162543%2Fc716ecd3-e141-4056-a1bf-6825a4b090d6.jpg</url>
      <title>DEV Community: Setasena Randata</title>
      <link>https://dev.to/setasena_randata_1cfa30f4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/setasena_randata_1cfa30f4"/>
    <language>en</language>
    <item>
      <title>I Built a Typing Game to Help People Discover Open Source</title>
      <dc:creator>Setasena Randata</dc:creator>
      <pubDate>Wed, 18 Feb 2026 18:15:07 +0000</pubDate>
      <link>https://dev.to/setasena_randata_1cfa30f4/i-built-a-typing-game-to-help-people-discover-open-source-3b6e</link>
      <guid>https://dev.to/setasena_randata_1cfa30f4/i-built-a-typing-game-to-help-people-discover-open-source-3b6e</guid>
      <description>&lt;h2&gt;
  
  
  I Love Open Source. Like, Really Love It.
&lt;/h2&gt;

&lt;p&gt;Let me be honest,&lt;br&gt;
I'm obsessed with open source.&lt;/p&gt;

&lt;p&gt;We're living in the &lt;strong&gt;vibe coding era&lt;/strong&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Psst...There's a time actually, where my client submit bug report to me, and honestly, opening claude code through my claude app from my phone, solves it, and I'm inside a mall toilet btw.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI can now spin up a SaaS product in a weekend. Tools that used to take months to build? An afternoon with the right prompt. And in a world where anyone can ship anything, I believe &lt;strong&gt;open source is the answer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because open source has something no AI-generated SaaS can replicate: &lt;strong&gt;community&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Community shapes the product. Community makes it mature. Community keeps it honest.&lt;/p&gt;

&lt;p&gt;I'm always fascinated when I look at projects like &lt;a href="https://github.com/odoo/odoo" rel="noopener noreferrer"&gt;Odoo&lt;/a&gt; — thousands of pull requests, hundreds of GitHub Actions workflows, an ocean of issues and discussions. Real people building real things together. It's just amazing.&lt;/p&gt;

&lt;p&gt;That fascination is exactly why I built &lt;strong&gt;&lt;a href="https://codakey.io" rel="noopener noreferrer"&gt;Codakey&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Codakey?
&lt;/h2&gt;

&lt;p&gt;Codakey is a platform where you &lt;strong&gt;learn open-source projects by playing a typing game&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Pick any project, React, Next.js, Drizzle, whatever interests you, and play through an interactive journey of typing challenges and clicker gameplay built around real code from that project. Instead of passively reading docs, you type actual code, learn how projects are structured, and build muscle memory for real patterns.&lt;/p&gt;

&lt;p&gt;You earn XP, level up, unlock upgrades, and compete on leaderboards. It's learning disguised as gaming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The game feature is still a work in progress&lt;/strong&gt;, but the platform is live. And here's the thing — &lt;strong&gt;I welcome all of you to submit your project&lt;/strong&gt;. Seriously. Whether it's your weekend side project or a tool with 10k stars, submit it. Later on, I hope Codakey will help someone understand &lt;em&gt;your&lt;/em&gt; project through a game experience — gamification, discovery, and real learning.&lt;/p&gt;

&lt;p&gt;You can even build your own &lt;strong&gt;Arsenal&lt;/strong&gt; — a personal collection of the open-source tools you love and use. Think of it as your curated open-source toolkit, tailored to &lt;em&gt;you&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Team
&lt;/h2&gt;

&lt;p&gt;This is a project from me, &lt;a href="https://setasena.com" rel="noopener noreferrer"&gt;Setasena&lt;/a&gt;, and my friend &lt;a href="https://raissabilullah.my.id/" rel="noopener noreferrer"&gt;Rais&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And yes... if you visit Rais' site, you can tell. It's a game. The game is built by him, and I do the boring stuff, I guess? Frontend, backend, and infrastructure. Haha.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack (for the Curious)
&lt;/h2&gt;

&lt;p&gt;We're running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router) + React 19 + TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phaser 3&lt;/strong&gt; for the game engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; + Drizzle ORM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt; (Pages, KV, Workers, Queues) for the entire infra&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; + Framer Motion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codakey.io/projects" rel="noopener noreferrer"&gt;Browse projects on Codakey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.codakey.io" rel="noopener noreferrer"&gt;Play the game&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.gg/codakey" rel="noopener noreferrer"&gt;Join our Discord&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have an open-source project you're proud of, &lt;strong&gt;&lt;a href="https://app.codakey.io/dashboard/submit" rel="noopener noreferrer"&gt;submit it&lt;/a&gt;&lt;/strong&gt;. Let's make it playable.&lt;/p&gt;

&lt;p&gt;Open source isn't just code. It's people. And I want to build something that celebrates that.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with love and way too much typing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Building Chalkboard: Open Source Billiard Hall Management</title>
      <dc:creator>Setasena Randata</dc:creator>
      <pubDate>Tue, 13 Jan 2026 05:06:47 +0000</pubDate>
      <link>https://dev.to/setasena_randata_1cfa30f4/building-chalkboard-open-source-billiard-hall-management-391c</link>
      <guid>https://dev.to/setasena_randata_1cfa30f4/building-chalkboard-open-source-billiard-hall-management-391c</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built an open-source billiard hall management system with Next.js 15, React 19, and PostgreSQL. It handles everything from table sessions to F&amp;amp;B orders, payments, and analytics. &lt;a href="https://railway.com/deploy/chalkboardid?referralCode=D8ivdW" rel="noopener noreferrer"&gt;Try it on Railway&lt;/a&gt; or &lt;a href="https://hub.docker.com/r/kugieapp/chalkboard" rel="noopener noreferrer"&gt;run it with Docker&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Running a billiard hall in Indonesia involves juggling multiple systems: table time tracking, F&amp;amp;B orders, payments, staff management, and inventory. Most solutions are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expensive SaaS with monthly fees&lt;/li&gt;
&lt;li&gt;Excel spreadsheets (yes, really)&lt;/li&gt;
&lt;li&gt;Custom solutions that can't be easily replicated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to build something that &lt;strong&gt;any billiard hall could deploy and own their data&lt;/strong&gt;, whether they're in Jakarta, Manila, or anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Source?
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://kugie.app" rel="noopener noreferrer"&gt;Kugie&lt;/a&gt;, our motto is "Scale Smarter, Not Harder." For small businesses, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No vendor lock-in&lt;/strong&gt; - Your data stays yours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy anywhere&lt;/strong&gt; - Railway, Docker, VPS, or even Windows standalone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customize freely&lt;/strong&gt; - Fork it and make it yours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community-driven&lt;/strong&gt; - Features that actual operators need&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The modern stack that just works&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="nx"&gt;js&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;TypeScript &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;because&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="nx"&gt;lives&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Drizzle&lt;/span&gt; &lt;span class="nx"&gt;ORM&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;PostgreSQL&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Tailwind&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;Shadcn&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;NextAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;authentication&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why These Choices?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Next.js 15 with App Router&lt;/strong&gt;: Server components give us fast initial loads - crucial for operators checking tables on slower Indonesian internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drizzle ORM&lt;/strong&gt;: After dealing with Prisma's bulk query limitations at scale, Drizzle's SQL-like syntax and better performance won me over. Plus, Drizzle Studio is fantastic for database debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;: Battle-tested, great JSON support for flexible F&amp;amp;B item properties, and works everywhere - from Neon serverless to local Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features I'm Proud Of
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Context-Aware F&amp;amp;B Orders&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Orders can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linked to table sessions&lt;/li&gt;
&lt;li&gt;Standalone counter orders&lt;/li&gt;
&lt;li&gt;Draft orders (for customers waiting for tables)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The schema handles all three contexts elegantly&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fnbOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fnb_orders&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="na"&gt;tableSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table_session_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;references&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;tableSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;references&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;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&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="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// draft → pending → completed&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;strong&gt;Flexible Deployment Options&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Setup Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;Cloud, zero config&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Self-hosted VPS&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows Standalone&lt;/td&gt;
&lt;td&gt;Local with auto-update&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Real-Time Analytics Without the Overhead&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Pre-calculated analytics stored in &lt;code&gt;order_analytics&lt;/code&gt; table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Revenue by hour/day/month&lt;/li&gt;
&lt;li&gt;Popular items and peak times&lt;/li&gt;
&lt;li&gt;Staff performance tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No need for expensive analytics services - just PostgreSQL doing what it does best.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges &amp;amp; Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Supporting Poor Internet Connectivity
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Many billiard halls in Indonesia have unreliable internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optimistic UI updates with local state&lt;/li&gt;
&lt;li&gt;Service Worker for offline capability (planned)&lt;/li&gt;
&lt;li&gt;Windows standalone that works 100% locally&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge 2: Multi-Language Support
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Staff might prefer Indonesian, but owners want English reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: next-intl with route-based locales (&lt;code&gt;/id/dashboard&lt;/code&gt; vs &lt;code&gt;/en/dashboard&lt;/code&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="c1"&gt;// Clean separation of concerns&lt;/span&gt;
&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;common&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;fnb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;common&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
    &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
    &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;fnb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Challenge 3: Complex Payment Flows
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: A single payment might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple table sessions&lt;/li&gt;
&lt;li&gt;Multiple F&amp;amp;B orders&lt;/li&gt;
&lt;li&gt;Split payments&lt;/li&gt;
&lt;li&gt;Tips&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Consolidated payment model with JSON metadata:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payments&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="na"&gt;totalAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;total_amount&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="na"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;metadata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Flexible structure for complex scenarios&lt;/span&gt;
  &lt;span class="na"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_method&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="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notNull&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;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm preparing to launch Chalkboard v1.0.3 widely. Planned features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile PWA&lt;/strong&gt; for table-side ordering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-location support&lt;/strong&gt; for chains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced inventory&lt;/strong&gt; with supplier management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Membership system&lt;/strong&gt; with loyalty points&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quick deploy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://railway.com/deploy/chalkboardid?referralCode=D8ivdW" rel="noopener noreferrer"&gt;Railway (1-click)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/r/kugieapp/chalkboard" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kugie-app/chalkboard.id" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Feedback welcome!&lt;/strong&gt; Whether you run a billiard hall, arcade, or any time-based rental business, I'd love to hear if this could work for you.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>buildinpublic</category>
      <category>typescript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Stop Fighting Sanity's Portable Text - Here's How to Use Markdown Instead</title>
      <dc:creator>Setasena Randata</dc:creator>
      <pubDate>Sun, 05 Oct 2025 11:06:29 +0000</pubDate>
      <link>https://dev.to/setasena_randata_1cfa30f4/stop-fighting-sanitys-portable-text-heres-how-to-use-markdown-instead-47cb</link>
      <guid>https://dev.to/setasena_randata_1cfa30f4/stop-fighting-sanitys-portable-text-heres-how-to-use-markdown-instead-47cb</guid>
      <description>&lt;p&gt;Let me tell you about the worst hour of my Tuesday.&lt;/p&gt;

&lt;p&gt;I wrote a 2,000-word blog post in Notion. Beautiful formatting, perfect headings, code blocks, the works. Then I tried to publish it to my Sanity CMS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One hour later&lt;/strong&gt;, I was manually converting this:&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="gu"&gt;## How to Use the API&lt;/span&gt;

Simply call the &lt;span class="sb"&gt;`fetchData()`&lt;/span&gt; function:

&lt;span class="se"&gt;\`\`\`&lt;/span&gt;javascript
const data = await fetchData()
&lt;span class="se"&gt;\`\`\`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Into this monstrosity:&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="nl"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;How to Use the API&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;children&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="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Simply call the &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="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetchData()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;javascript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;const data = await fetchData()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I literally said out loud: "There has to be a better way."&lt;/p&gt;

&lt;p&gt;Turns out, there is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Portable Text Makes You Want to Scream
&lt;/h2&gt;

&lt;p&gt;Don't get me wrong - Sanity is fantastic. The headless CMS architecture is brilliant, the API is clean, and the real-time collaboration is 🔥.&lt;/p&gt;

&lt;p&gt;But Portable Text? It's like they said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You know what developers love? Converting their perfectly good Markdown into deeply nested JSON objects by hand."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody. Nobody said that.&lt;/p&gt;

&lt;p&gt;The problem is that Portable Text is &lt;strong&gt;structured&lt;/strong&gt; content. It's designed to be platform-agnostic, queryable, and transformable. Which is great! Until you actually have to write content in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Markdown Plugin
&lt;/h2&gt;

&lt;p&gt;Here's the good news: Sanity has a markdown plugin. Here's the bad news: nobody talks about it.&lt;/p&gt;

&lt;p&gt;Let me show you exactly how to set it up in your Sanity project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Plugin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-sanity-studio

&lt;span class="c"&gt;# Install the markdown plugin&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;sanity-plugin-markdown easymde@2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for installation. Now for configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Update Your Sanity Config
&lt;/h3&gt;

&lt;p&gt;Open your &lt;code&gt;sanity.config.ts&lt;/code&gt; and add the markdown schema to your plugins:&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;defineConfig&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;sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;structureTool&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;sanity/structure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;markdownSchema&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;sanity-plugin-markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-project-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;structureTool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;markdownSchema&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// 👈 Add this line&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;yourSchemas&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;h3&gt;
  
  
  Step 3: Add Markdown Field to Your Schema
&lt;/h3&gt;

&lt;p&gt;Now update your post schema to include a markdown content field:&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;defineField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineType&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;sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&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="c1"&gt;// 🎯 The magic field&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write your content in beautiful Markdown&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="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;
  
  
  Step 4: Style the Editor (Optional but Recommended)
&lt;/h3&gt;

&lt;p&gt;The default markdown editor is... functional. Let's make it nice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install CSS dependencies&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;easymde@2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;sanity.css&lt;/code&gt; in your studio root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;'easymde/dist/easymde.min.css'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* Make it look less like 2010 */&lt;/span&gt;
&lt;span class="nc"&gt;.EasyMDEContainer&lt;/span&gt; &lt;span class="nc"&gt;.CodeMirror&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e2e8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'JetBrains Mono'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;'Fira Code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;monospace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.EasyMDEContainer&lt;/span&gt; &lt;span class="nc"&gt;.editor-toolbar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e2e8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f8fafc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.EasyMDEContainer&lt;/span&gt; &lt;span class="nc"&gt;.editor-toolbar&lt;/span&gt; &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#475569&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.EasyMDEContainer&lt;/span&gt; &lt;span class="nc"&gt;.editor-toolbar&lt;/span&gt; &lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e2e8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#cbd5e1&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;Import it in your studio's main file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;p&gt;After this setup, your Sanity Studio has a proper markdown editor with:&lt;/p&gt;

&lt;p&gt;✅ Live preview side-by-side&lt;br&gt;
✅ Toolbar for formatting (no more memorizing markdown syntax)&lt;br&gt;
✅ Code block support with syntax highlighting&lt;br&gt;
✅ Image insertion&lt;br&gt;
✅ Table support&lt;br&gt;
✅ Full keyboard shortcuts&lt;/p&gt;

&lt;p&gt;And here's the best part: &lt;strong&gt;it stores actual markdown text&lt;/strong&gt;, not Portable Text JSON.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rendering Markdown on Your Frontend
&lt;/h2&gt;

&lt;p&gt;Now when you query your Sanity content, you get clean markdown strings:&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="c1"&gt;// What you get from Sanity&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;## Introduction&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;This is **markdown**!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To render it in your Next.js/React app:&lt;br&gt;
&lt;/p&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;react-markdown remark-gfm rehype-raw rehype-sanitize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nx"&gt;ReactMarkdown&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;react-markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;remarkGfm&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;remark-gfm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;post&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="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&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;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReactMarkdown&lt;/span&gt;
        &lt;span class="nx"&gt;remarkPlugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;&lt;span class="nx"&gt;remarkGfm&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prose prose-lg&lt;/span&gt;&lt;span class="dl"&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;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ReactMarkdown&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/article&lt;/span&gt;&lt;span class="err"&gt;&amp;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;Boom. Clean, simple, no Portable Text serializers required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Gotcha #1: "Unknown type: markdown"
&lt;/h3&gt;

&lt;p&gt;If you see this error, you forgot to add &lt;code&gt;markdownSchema()&lt;/code&gt; to your plugins array in &lt;code&gt;sanity.config.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha #2: Editor looks broken
&lt;/h3&gt;

&lt;p&gt;Make sure you're importing the CSS. The easymde styles are required:&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;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;easymde/dist/easymde.min.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gotcha #3: Images not working
&lt;/h3&gt;

&lt;p&gt;Markdown image syntax expects URLs:&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="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Alt text&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/image.jpg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to use Sanity's image CDN, you'll need to either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload images separately and use the CDN URL&lt;/li&gt;
&lt;li&gt;Stick with Portable Text for the image field&lt;/li&gt;
&lt;li&gt;Use a mixed approach (markdown for content, separate image fields)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Look, I get it. Portable Text is "the right way" to do structured content. But sometimes you just want to write a damn blog post without feeling like you're programming JSON.&lt;/p&gt;

&lt;p&gt;Markdown gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; - Write content at the speed of thought&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt; - Works everywhere, from GitHub to Notion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt; - Everyone knows markdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less friction&lt;/strong&gt; - Copy-paste actually works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is it perfect? No. But it's a hell of a lot better than manually building nested block objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Working Example
&lt;/h2&gt;

&lt;p&gt;I've set up a demo repo with a complete working Sanity Studio using markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the example&lt;/span&gt;
git clone https://github.com/yourusername/sanity-markdown-example
&lt;span class="nb"&gt;cd &lt;/span&gt;sanity-markdown-example

&lt;span class="c"&gt;# Install and run&lt;/span&gt;
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schema included, styles configured, ready to customize.&lt;/p&gt;

&lt;h2&gt;
  
  
  But Wait - There's an Even Better Way
&lt;/h2&gt;

&lt;p&gt;Here's where I drop the pitch.&lt;/p&gt;

&lt;p&gt;If you're:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing lots of blog content&lt;/li&gt;
&lt;li&gt;Managing multiple Sanity projects&lt;/li&gt;
&lt;li&gt;Doing content work for clients&lt;/li&gt;
&lt;li&gt;Tired of the content → CMS workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might want to check out &lt;a href="https://terradium.io" rel="noopener noreferrer"&gt;&lt;strong&gt;Terradium&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's an AI-powered content platform I built that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generates SEO-optimized content&lt;/strong&gt; using multi-agent AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatically publishes to Sanity&lt;/strong&gt; in markdown format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles the entire workflow&lt;/strong&gt; from research to publishing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "Write 5 blog posts about Next.js 15 features"
     ↓
Terradium AI:
  → Researches keywords
  → Generates outlines
  → Writes full articles
  → Optimizes for SEO
  → Publishes to your Sanity project
     ↓
You: Review and click publish (or let it auto-publish)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Time saved:&lt;/strong&gt; ~5 hours per article&lt;br&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; Transparent "electron" credits (~$0.50 per full article)&lt;br&gt;
&lt;strong&gt;Result:&lt;/strong&gt; Professional content in your Sanity CMS, ready to review&lt;/p&gt;

&lt;p&gt;The free tier gives you 30 electrons (~5 full articles) to try it out. No credit card required.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why I Built This
&lt;/h3&gt;

&lt;p&gt;After manually creating hundreds of blog posts for clients, I realized:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The research phase is 80% the same every time&lt;/li&gt;
&lt;li&gt;SEO optimization follows predictable patterns&lt;/li&gt;
&lt;li&gt;Markdown → Sanity conversion should be automated&lt;/li&gt;
&lt;li&gt;The "publish to CMS" step is pure busywork&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I automated all of it with AI agents.&lt;/p&gt;

&lt;p&gt;Now I use Terradium to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate client blog content in bulk&lt;/li&gt;
&lt;li&gt;Create documentation faster&lt;/li&gt;
&lt;li&gt;Populate demo projects with real content&lt;/li&gt;
&lt;li&gt;Test Sanity schemas with actual articles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It integrates directly with your Sanity project (using the markdown setup we just configured!) and handles everything from keyword research to final publishing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Try It Out
&lt;/h3&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/YrO-Hwmgwgc"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;If you just spent an hour following this tutorial to set up markdown support, you're &lt;strong&gt;exactly&lt;/strong&gt; the person who would benefit from Terradium.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://terradium.io" rel="noopener noreferrer"&gt;Try Terradium Free&lt;/a&gt;&lt;/strong&gt; - 30 electrons included, no credit card&lt;/p&gt;

&lt;p&gt;Use your newly configured Sanity + Markdown setup, connect your project credentials, and generate your first AI-powered blog post in ~10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Whether you use the markdown plugin manually or automate it with Terradium, the key takeaway is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't have to fight Portable Text.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Markdown is a perfectly valid option for Sanity CMS. It's faster to write, easier to migrate, and frankly, way more pleasant to work with.&lt;/p&gt;

&lt;p&gt;Set it up once, and never manually convert content again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions?
&lt;/h2&gt;

&lt;p&gt;Drop them in the comments! I'm happy to help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sanity + markdown setup issues&lt;/li&gt;
&lt;li&gt;Frontend rendering questions&lt;/li&gt;
&lt;li&gt;Migration from Portable Text to markdown&lt;/li&gt;
&lt;li&gt;Terradium integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you found this helpful, consider giving Terradium a try. The free tier is genuinely useful (I'm not just saying that - it's 10 full articles).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Sanity's Portable Text is pain. Install &lt;code&gt;sanity-plugin-markdown&lt;/code&gt;, add &lt;code&gt;markdownSchema()&lt;/code&gt; to your config, use &lt;code&gt;type: 'markdown'&lt;/code&gt; in your schema. Write content like a normal person. Or automate the whole thing with &lt;a href="https://terradium.io" rel="noopener noreferrer"&gt;Terradium&lt;/a&gt; and never write manually again.&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>sanity</category>
      <category>markdown</category>
      <category>tutorial</category>
      <category>howto</category>
    </item>
    <item>
      <title>I Spent 6 Hours Per Blog Post Until I Built This AI Content Platform</title>
      <dc:creator>Setasena Randata</dc:creator>
      <pubDate>Sun, 05 Oct 2025 10:53:13 +0000</pubDate>
      <link>https://dev.to/setasena_randata_1cfa30f4/i-spent-6-hours-per-blog-post-until-i-built-this-ai-content-platform-2m14</link>
      <guid>https://dev.to/setasena_randata_1cfa30f4/i-spent-6-hours-per-blog-post-until-i-built-this-ai-content-platform-2m14</guid>
      <description>&lt;p&gt;Three months ago, I was billing clients $25-40 per blog post. Sounds great, right? Wrong.&lt;/p&gt;

&lt;p&gt;Each post took me 4-6 hours of work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1-2 hours researching keywords and competitors&lt;/li&gt;
&lt;li&gt;2-3 hours writing and editing&lt;/li&gt;
&lt;li&gt;1 hour (yes, ONE HOUR) manually converting Markdown to Sanity's Portable Text format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part? That's what broke me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Portable Text Problem
&lt;/h2&gt;

&lt;p&gt;If you've ever worked with Sanity CMS, you know the pain. Portable Text is powerful, but converting content manually is soul-crushing:&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="nl"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;children&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="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Just trying to format a simple paragraph...&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiply that by 2000 words, add images, links, headings, and you're looking at serious time sink.&lt;/p&gt;

&lt;p&gt;I tried existing tools. They either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broke on complex formatting&lt;/li&gt;
&lt;li&gt;Didn't support Sanity properly&lt;/li&gt;
&lt;li&gt;Required manual cleanup anyway&lt;/li&gt;
&lt;li&gt;Cost more than they saved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built my own solution. Then I kept building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Terradium
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/YrO-Hwmgwgc"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;What started as a "convert Markdown to Portable Text" script evolved into a full AI-powered content platform.&lt;/p&gt;

&lt;p&gt;Here's what it does now:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. AI Multi-Agent System
&lt;/h3&gt;

&lt;p&gt;I implemented a multi-agent architecture where specialized AI agents handle different parts of content creation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Master Coordinator&lt;/strong&gt; - Plans the entire content strategy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO Research Agent&lt;/strong&gt; - Uses Perplexity AI for real-time competitive analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Writer&lt;/strong&gt; - Generates SEO-optimized articles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Improver&lt;/strong&gt; - Reviews and enhances quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity Publisher&lt;/strong&gt; - Handles the dreaded Portable Text conversion automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Content Planning at Scale
&lt;/h3&gt;

&lt;p&gt;Instead of one-off articles, you can generate entire content strategies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input: "AI tools for developers"
Output: 12-article content plan with:
- Keyword research
- Topic clusters
- Publishing schedule
- SEO optimization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Electron System
&lt;/h3&gt;

&lt;p&gt;I got tired of unpredictable AI costs, so I built a transparent usage system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every operation costs "electrons"&lt;/li&gt;
&lt;li&gt;Real-time balance tracking&lt;/li&gt;
&lt;li&gt;No surprises in your bill&lt;/li&gt;
&lt;li&gt;Mix subscriptions with one-time booster packs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: Full article with SEO research = 6 electrons (~$0.40-0.60 depending on your plan)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;p&gt;Built with modern tools developers actually want to use:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15 with App Router&lt;/li&gt;
&lt;li&gt;TypeScript (because we're not animals)&lt;/li&gt;
&lt;li&gt;TailwindCSS v4&lt;/li&gt;
&lt;li&gt;Shadcn/ui components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convex for real-time reactive database&lt;/li&gt;
&lt;li&gt;Clerk for auth (JWT integration was smooth)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Feature:&lt;/strong&gt;&lt;br&gt;
Everything is real-time. You literally watch the AI agents work through each step. No black box, no waiting.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Streaming AI Responses Are A Game-Changer
&lt;/h3&gt;

&lt;p&gt;E.g., Using OpenAI's streaming API instead of waiting for complete responses:&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;stream&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
  &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Update UI in real-time&lt;/span&gt;
  &lt;span class="nf"&gt;updateProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&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;Users see progress immediately. Huge UX win.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Convex Made Real-Time Stupidly Easy
&lt;/h3&gt;

&lt;p&gt;Coming from traditional REST APIs, Convex's reactive queries were mind-blowing:&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;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Automatically updates when data changes. That's it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No websockets. No polling. No Redux hell.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI Agents Need Guardrails
&lt;/h3&gt;

&lt;p&gt;Early versions would go off the rails. The solution? Structured outputs and validation:&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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 agents can be creative within constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Beta Launch
&lt;/h2&gt;

&lt;p&gt;I'm launching Terradium in beta at &lt;strong&gt;terradium.io&lt;/strong&gt; this week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works:&lt;/strong&gt;&lt;br&gt;
✅ AI content generation with multi-agent system&lt;br&gt;
✅ Automatic Sanity CMS publishing&lt;br&gt;
✅ Content planning and bulk generation&lt;br&gt;
✅ Real-time progress tracking&lt;br&gt;
✅ Transparent electron-based pricing&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's coming:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More CMS integrations (WordPress, Contentful)&lt;/li&gt;
&lt;li&gt;Custom AI model support&lt;/li&gt;
&lt;li&gt;Team collaboration features&lt;/li&gt;
&lt;li&gt;Advanced analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pricing Philosophy
&lt;/h2&gt;

&lt;p&gt;I wanted pricing that doesn't feel like highway robbery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free tier:&lt;/strong&gt; 30 electrons (~10 articles) to try it out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fusion:&lt;/strong&gt; $19/mo for 270 electrons (~90 articles)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reactor:&lt;/strong&gt; $49/mo for 800 electrons (~265 articles)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus one-time electron packs if you need a burst of content.&lt;/p&gt;

&lt;p&gt;Compare that to hiring writers at $100-400 per article. Even if you use this for 10 articles a month, it pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you're:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spending hours on content creation&lt;/li&gt;
&lt;li&gt;Fighting with CMS formatting&lt;/li&gt;
&lt;li&gt;Doing client content work&lt;/li&gt;
&lt;li&gt;Running a dev blog or documentation site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give Terradium a shot. The free tier is actually generous (30 electrons), and I'd love your feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beta access:&lt;/strong&gt; &lt;a href="https://terradium.io" rel="noopener noreferrer"&gt;terradium.io&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Questions for the Community
&lt;/h2&gt;

&lt;p&gt;I'm still figuring some things out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Would you want this to work with static site generators (Hugo, Gatsby)?&lt;/li&gt;
&lt;li&gt;Is the electron system intuitive or confusing?&lt;/li&gt;
&lt;li&gt;What other CMS platforms should I prioritize?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop your thoughts in the comments. Seriously, I'm one developer trying to solve a real problem I had. Your feedback shapes where this goes.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Built an AI platform that turns 6-hour blog posts into 10-minute automated workflows. Uses multi-agent AI system, automatically converts to Sanity Portable Text, transparent electron-based pricing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>sanity</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Hi! I'm tired finding a self hosted finance tool, so I make one.</title>
      <dc:creator>Setasena Randata</dc:creator>
      <pubDate>Wed, 14 May 2025 12:41:07 +0000</pubDate>
      <link>https://dev.to/setasena_randata_1cfa30f4/hi-im-tired-finding-a-self-hosted-finance-tool-so-i-make-one-21dn</link>
      <guid>https://dev.to/setasena_randata_1cfa30f4/hi-im-tired-finding-a-self-hosted-finance-tool-so-i-make-one-21dn</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/setasena_randata_1cfa30f4/summit-finance-a-modern-open-source-invoicing-solution-built-with-nextjs-drizzle-orm-and-1da4" class="crayons-story__hidden-navigation-link"&gt;Summit Finance: A Modern Open Source Invoicing Solution Built with Next.js, Drizzle ORM, and Tailwind CSS&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/setasena_randata_1cfa30f4" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F3162543%2Fc716ecd3-e141-4056-a1bf-6825a4b090d6.jpg" alt="setasena_randata_1cfa30f4 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/setasena_randata_1cfa30f4" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Setasena Randata
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Setasena Randata
                
              
              &lt;div id="story-author-preview-content-2487880" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/setasena_randata_1cfa30f4" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F3162543%2Fc716ecd3-e141-4056-a1bf-6825a4b090d6.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Setasena Randata&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/setasena_randata_1cfa30f4/summit-finance-a-modern-open-source-invoicing-solution-built-with-nextjs-drizzle-orm-and-1da4" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 14 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/setasena_randata_1cfa30f4/summit-finance-a-modern-open-source-invoicing-solution-built-with-nextjs-drizzle-orm-and-1da4" id="article-link-2487880"&gt;
          Summit Finance: A Modern Open Source Invoicing Solution Built with Next.js, Drizzle ORM, and Tailwind CSS
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/setasena_randata_1cfa30f4/summit-finance-a-modern-open-source-invoicing-solution-built-with-nextjs-drizzle-orm-and-1da4" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;10&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/setasena_randata_1cfa30f4/summit-finance-a-modern-open-source-invoicing-solution-built-with-nextjs-drizzle-orm-and-1da4#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Summit Finance: A Modern Open Source Invoicing Solution Built with Next.js, Drizzle ORM, and Tailwind CSS</title>
      <dc:creator>Setasena Randata</dc:creator>
      <pubDate>Wed, 14 May 2025 12:37:50 +0000</pubDate>
      <link>https://dev.to/setasena_randata_1cfa30f4/summit-finance-a-modern-open-source-invoicing-solution-built-with-nextjs-drizzle-orm-and-1da4</link>
      <guid>https://dev.to/setasena_randata_1cfa30f4/summit-finance-a-modern-open-source-invoicing-solution-built-with-nextjs-drizzle-orm-and-1da4</guid>
      <description>&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%2Fvek12387pcicxrogi4nc.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%2Fvek12387pcicxrogi4nc.png" alt="Image description" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Built Summit: A Modern Open Source Invoicing Solution
&lt;/h2&gt;

&lt;p&gt;As a small agency, finding the right financial management tool was a journey filled with disappointments. We've tried several options over the years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Akaunting&lt;/strong&gt;: Comprehensive but we weren't familiar with PHP, making customizations challenging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InvoiceNinja&lt;/strong&gt;: Offered limited functionality for our specific workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crater&lt;/strong&gt;: Nice interface but didn't fit our exact needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twenty CRM&lt;/strong&gt;: Too resource-intensive, consuming excessive CPU and RAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our requirements were simple: we just wanted a tool that helps us create quotations, send invoices through email, and automate payments. That's how Summit was born – a lightweight, modern invoicing solution built with technologies we love.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summit: The Tech Stack Breakdown
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://summitfinance.app" rel="noopener noreferrer"&gt;Summit Finance&lt;/a&gt; is built with a modern JavaScript stack optimized for developer experience and performance:&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Framework: Next.js (App Router)
&lt;/h3&gt;

&lt;p&gt;Summit leverages Next.js with its App Router architecture, bringing several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server Components&lt;/strong&gt;: Reducing client-side JavaScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Routing&lt;/strong&gt;: With nested layouts and loading states&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced Performance&lt;/strong&gt;: Through automatic optimizations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Routes&lt;/strong&gt;: Simplified backend functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The App Router's structured approach helps maintain a clean codebase even as the application grows in complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Language: TypeScript
&lt;/h3&gt;

&lt;p&gt;We chose TypeScript for the obvious benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety&lt;/strong&gt;: Catching errors before they reach production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced Developer Experience&lt;/strong&gt;: Better autocomplete and IntelliSense&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Documenting Code&lt;/strong&gt;: Types serve as documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier Refactoring&lt;/strong&gt;: Making large-scale changes with confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a financial application where accuracy is critical, TypeScript's strictness is a major advantage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database Layer: PostgreSQL + Drizzle ORM
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Why PostgreSQL?
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: Rock-solid stability for financial data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Features&lt;/strong&gt;: JSON support, full-text search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Integrity&lt;/strong&gt;: Strong constraints and relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Handles growing datasets efficiently&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why Drizzle ORM?
&lt;/h4&gt;

&lt;p&gt;Drizzle ORM is a relative newcomer in the TypeScript ORM space, but we chose it for several reasons:&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="c1"&gt;// Example Drizzle schema definition&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoiceSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;defaultRandom&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;invoiceNumber&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice_number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;references&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;clientSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;due_date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;status&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&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="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`status in ('draft', 'sent', 'paid', 'overdue', 'cancelled')`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;defaultNow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;notNull&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety&lt;/strong&gt;: First-class TypeScript support with minimal boilerplate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Lightweight with no hidden N+1 queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Experience&lt;/strong&gt;: SQL-like syntax that feels natural&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema Management&lt;/strong&gt;: Easy migrations and schema changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Building&lt;/strong&gt;: Intuitive API for complex queries&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  UI Layer: Tailwind CSS + shadcn/ui
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Why Tailwind CSS?
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Utility-First&lt;/strong&gt;: Rapid UI development with minimal context-switching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: Predefined design system scales&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Small bundle size with only the CSS you use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsiveness&lt;/strong&gt;: Built-in responsive design patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why shadcn/ui?
&lt;/h4&gt;

&lt;p&gt;shadcn/ui provides high-quality, accessible components built on Radix UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example shadcn/ui usage in Summit&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Invoice #INV-2023-0042&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Due on April 30, 2023&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"font-medium"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Client: Acme Inc.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;$1,250.00&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardFooter&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-end"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Mark as Paid&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardFooter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy-Paste Components&lt;/strong&gt;: No dependencies, just copy the code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable&lt;/strong&gt;: Full control over the implementation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessible&lt;/strong&gt;: Built with accessibility in mind&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beautiful Design&lt;/strong&gt;: Professional and modern aesthetics&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication: NextAuth.js
&lt;/h3&gt;

&lt;p&gt;NextAuth.js provides a comprehensive authentication solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Providers&lt;/strong&gt;: Email/password, OAuth, magic links&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session Management&lt;/strong&gt;: Secure, JWT-based sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-Based Access&lt;/strong&gt;: Team management with different permission levels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Built-in CSRF protection and security best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Form Handling: React Hook Form + Zod
&lt;/h3&gt;

&lt;p&gt;This combination creates a powerful form system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example form setup in Summit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Client name is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Amount must be positive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Due date must be in the future&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useForm&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;formSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;zodResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formSchema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;defaultValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dueDate&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;Date&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Minimal re-renders with precise updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt;: Type-safe schema validation with Zod&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Experience&lt;/strong&gt;: Intuitive API and great dev tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt;: Built with accessibility in mind&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Email: Resend
&lt;/h3&gt;

&lt;p&gt;Resend provides a modern API for sending transactional emails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Developer-Friendly&lt;/strong&gt;: Simple API with great TypeScript support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable Delivery&lt;/strong&gt;: High deliverability rates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt;: Track opens, clicks, and deliveries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates&lt;/strong&gt;: React-based email templates with JSX&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Payments: Xendit
&lt;/h3&gt;

&lt;p&gt;For those in Southeast Asia, Xendit offers comprehensive payment processing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Payment Methods&lt;/strong&gt;: Cards, bank transfers, e-wallets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Reconciliation&lt;/strong&gt;: Track payments against invoices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt;: Real-time payment notifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt;: PCI-DSS compliant&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  File Storage: MinIO / S3
&lt;/h3&gt;

&lt;p&gt;For receipt and document storage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt;: Self-hosted MinIO or any S3-compatible service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Handles growing storage needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Fast uploads and downloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Access control and encryption&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Features That Make Summit Shine
&lt;/h2&gt;

&lt;p&gt;Summit provides everything a small business needs for financial management:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete Invoicing Workflow&lt;/strong&gt;: Create, send, track, and receive payments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quoting System&lt;/strong&gt;: Create quotes and convert to invoices when accepted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expense and Income Tracking&lt;/strong&gt;: Complete financial overview&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Portal&lt;/strong&gt;: Let clients view and pay invoices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team Collaboration&lt;/strong&gt;: Role-based access for your team&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial Reporting&lt;/strong&gt;: Get insights into your business finances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring Transactions&lt;/strong&gt;: Automate recurring invoices&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why You Might Want to Try Summit
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Familiar Tech Stack&lt;/strong&gt;: If you're already comfortable with React/Next.js, you'll feel right at home&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hosted&lt;/strong&gt;: Keep your financial data under your control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Source&lt;/strong&gt;: Customize it to fit your exact needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern UX&lt;/strong&gt;: Clean, responsive interface built with today's best practices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt;: No bloat, just the features you need&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Deploying Summit
&lt;/h2&gt;

&lt;p&gt;Summit is designed to be easy to deploy, with one-click deployment options for platforms like Railway. The setup process is well-documented, and the environment variables are clearly explained.&lt;/p&gt;

&lt;p&gt;If you're looking to try Summit, you can follow the comprehensive deployment guide available in the repository, which takes you through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setting up the database&lt;/li&gt;
&lt;li&gt;Configuring MinIO for file storage&lt;/li&gt;
&lt;li&gt;Connecting email services&lt;/li&gt;
&lt;li&gt;Setting up payment processing&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Future Plans
&lt;/h2&gt;

&lt;p&gt;The project is actively maintained by the team at &lt;a href="https://kugie.app" rel="noopener noreferrer"&gt;Kugie.app&lt;/a&gt;, with plans to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More payment gateways beyond Xendit&lt;/li&gt;
&lt;li&gt;Enhanced reporting capabilities&lt;/li&gt;
&lt;li&gt;Mobile application&lt;/li&gt;
&lt;li&gt;Additional automation features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Summit represents a modern approach to invoicing and financial management, built with technologies that prioritize developer experience, performance, and user experience. If you've been frustrated with existing solutions, especially if you're already familiar with the React ecosystem, Summit might be exactly what you've been looking for.&lt;/p&gt;

&lt;p&gt;Give Summit a try, and if you like it, consider contributing to the project! Open source thrives on community involvement, and your expertise could help make Summit even better.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://summitfinance.app" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/kugie-app/summit" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://kugie.dev/summit-roadmap" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Have you tried Summit or thinking about giving it a go? Let me know in the comments!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
