<?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: UnderPeaks</title>
    <description>The latest articles on DEV Community by UnderPeaks (@under_peaks).</description>
    <link>https://dev.to/under_peaks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032518%2F74f074fa-a701-483a-9de7-d39e441e29ff.PNG</url>
      <title>DEV Community: UnderPeaks</title>
      <link>https://dev.to/under_peaks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/under_peaks"/>
    <language>en</language>
    <item>
      <title>A headless CMS that only gives you an API is only half a product</title>
      <dc:creator>UnderPeaks</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:19:51 +0000</pubDate>
      <link>https://dev.to/under_peaks/a-headless-cms-that-only-gives-you-an-api-is-only-half-a-product-2lkk</link>
      <guid>https://dev.to/under_peaks/a-headless-cms-that-only-gives-you-an-api-is-only-half-a-product-2lkk</guid>
      <description>&lt;p&gt;This is how most headless CMS projects go for me. I pick a tool, set up my content models, and get an API. Nice. Then I sit down to build the actual app.&lt;/p&gt;

&lt;p&gt;List screen. Detail screen. A form with validation. Loading states, error states, empty states, search, pagination, login. Then I do it all again for the next model, and again on the next project. It’s the same twenty screens every time, and the API didn’t help with any of them.&lt;/p&gt;

&lt;p&gt;For years I built apps for small businesses, and almost every one needed the same things: a list of records, a detail page, an add/edit form and a login. Different clients, different logos, same screens. I must have built that set well over a dozen times before it finally annoyed me enough to do something about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Why not just reuse your code?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did. Shared widgets, a component library, a starter project I copied from job to job. It helped with the pieces, like buttons, inputs and layouts. But it never saved me the real work, because every client’s data was different. Different fields, different validation, different relationships. So I still ended up editing and customising every screen by hand, every time. A shared library can’t know what your data looks like.&lt;/p&gt;

&lt;p&gt;A generator can, because it reads your models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API should be where it starts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’ve been building Underpeaks for about a year and a half now, and one idea keeps coming back to me: the API should be where a backend tool starts. It shouldn’t be where it stops.&lt;/p&gt;

&lt;p&gt;If I’ve already told the tool what a Product is, with its fields, types and relationships, then it knows enough to build the screen that lists products, the form that edits them, and the code that talks to the API. Making me write all that by hand is just a choice the tool made.&lt;/p&gt;

&lt;p&gt;So Underpeaks generates real Flutter and Next.js apps from your models. One model, two apps, with the data layer already wired up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The elephant in the room&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let me be straight about what Underpeaks is. It’s a backend. It doesn’t build your product for you.&lt;/p&gt;

&lt;p&gt;The generated app is a starting point. You’ll still design your own UI, add your own business logic, and change things to suit your users. I’m not promising a finished app. I’m promising you skip the boring first 80% of wiring screens to an API, so you can get to the part that’s actually yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Generated code is a trap”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’ve heard that a lot, and I half agree. Most generators have the same three problems. You edit the output, regenerate, and lose your changes. The result looks generic. And you end up stuck with whatever architecture the tool decided on.&lt;/p&gt;

&lt;p&gt;Those are fair complaints. I just don’t think the fix is to stop generating code. I think it’s being honest about what generation is for.&lt;/p&gt;

&lt;p&gt;For me it’s a head start. The code is normal code and it’s yours, so you can open it, change it, and walk away from the tool tomorrow if you want. The API is still sitting there too. If you don’t like the generated screens, replace them or ignore them and build your own with any HTTP client.&lt;/p&gt;

&lt;p&gt;The bit I haven’t cracked yet is keeping generated files and your own files properly separate, so regenerating never wipes out your work. I’m still working on that one, and honestly I’d like to hear how you’d want it to behave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just ask an AI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fair question, and it’s a good option. But AI gives you something a bit different every time, and a generator doesn’t. If I rename a field on a model, I want every screen that uses it to update the same way, not get a best guess. For me, generation covers the repeatable part, and AI is great for everything you build on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I’ve landed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A headless CMS shouldn’t stop at serving data. It should give you the data, the API and a working starting point, and then get out of your way. My test is simple: if you could leave the tool tomorrow and still keep your app, then generating code isn’t a trap. It’s just time saved.&lt;/p&gt;

&lt;p&gt;Underpeaks Core is free and self-hosted if you want to have a look: [&lt;a href="https://github.com/underpeaks/underpeaks-core" rel="noopener noreferrer"&gt;https://github.com/underpeaks/underpeaks-core&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’d like to know where you stand on this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Would you rather get just an API, or a working app to start from?&lt;/li&gt;
&lt;li&gt;What would make you trust generated code enough to ship it?&lt;/li&gt;
&lt;li&gt;What’s burned you before with a code generator?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m building this in the open, so what you say really does shape what I do next.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cms</category>
      <category>flutter</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Install Underpeaks Core: A Free, Self-Hosted Headless CMS for Flutter and Next.js</title>
      <dc:creator>UnderPeaks</dc:creator>
      <pubDate>Tue, 06 Oct 2026 08:13:39 +0000</pubDate>
      <link>https://dev.to/under_peaks/install-underpeaks-core-a-free-self-hosted-headless-cms-for-flutter-and-nextjs-1al0</link>
      <guid>https://dev.to/under_peaks/install-underpeaks-core-a-free-self-hosted-headless-cms-for-flutter-and-nextjs-1al0</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/T6wojyT-XZQ" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you've ever built the same app for web and mobile, you know the tax: two codebases, two sets of backend glue code, and one data model you keep in sync by hand. Change a field, update it in both places, miss one, ship a bug.&lt;/p&gt;

&lt;p&gt;Underpeaks Core is a free, open-source, self-hosted headless CMS built to get rid of that. You define your data model once, and it gives you a backend, a content editor and a REST API. It can also generate working Flutter and Next.js apps from the same model. It's compiled, not AI-generated, so the same model always gives you the same code.&lt;/p&gt;

&lt;p&gt;The video above shows the whole install. Here's the written version, so you can follow along or come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js and npm&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;A database: Supabase, PostgreSQL, MySQL, MongoDB or Firebase. Have the connection details ready.&lt;/li&gt;
&lt;li&gt;A free Underpeaks account for the license key (step 5)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Clone and install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/underpeaks/underpeaks-core.git
&lt;span class="nb"&gt;cd &lt;/span&gt;underpeaks-core
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Start Core
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt;. On a fresh install you're taken straight to the installer.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Run the installer
&lt;/h2&gt;

&lt;p&gt;You don't write any environment variables by hand. The installer does it for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose your database&lt;/li&gt;
&lt;li&gt;Enter your connection details (it writes your &lt;code&gt;.env&lt;/code&gt; for you)&lt;/li&gt;
&lt;li&gt;It tests the connection&lt;/li&gt;
&lt;li&gt;It creates the system tables&lt;/li&gt;
&lt;li&gt;Optionally, it seeds demo content so you have something to explore&lt;/li&gt;
&lt;li&gt;You create your admin account&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When it's done, you're sent to the sign-in page.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Sign in
&lt;/h2&gt;

&lt;p&gt;Use the admin email and password you just created.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add your license key
&lt;/h2&gt;

&lt;p&gt;The first time you sign in, Core asks for a license key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://underpeaks.com/signup?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=install-core" rel="noopener noreferrer"&gt;Create a free Underpeaks account&lt;/a&gt;. It takes about 30 seconds.&lt;/li&gt;
&lt;li&gt;Copy the license key from your dashboard.&lt;/li&gt;
&lt;li&gt;Paste it into Core.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then you land in the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Look around
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared Models&lt;/td&gt;
&lt;td&gt;Define your data models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages&lt;/td&gt;
&lt;td&gt;Build pages from your models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Menu&lt;/td&gt;
&lt;td&gt;Control the admin sidebar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigator&lt;/td&gt;
&lt;td&gt;Connect pages to each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme&lt;/td&gt;
&lt;td&gt;Colours and branding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Upload and manage media&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Users&lt;/td&gt;
&lt;td&gt;Manage who can access your instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settings&lt;/td&gt;
&lt;td&gt;Project name, license key, SMTP, API keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A good first move is to create a model and attach it to a page. The admin views for it show up straight away.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Generate your apps
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; underpeaks-nxf
nxf login
nxf generate flutter   &lt;span class="c"&gt;# or: nxf generate nextjs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nxf login&lt;/code&gt; opens your browser to connect the CLI to your account. After that, &lt;code&gt;generate&lt;/code&gt; gives you a real project you own, with models, an API client and auth flows included.&lt;/p&gt;

&lt;h2&gt;
  
  
  If something goes wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The installer can't connect:&lt;/strong&gt; check your connection details, and make sure your database accepts connections from where Core is running. Cloud databases often need your IP address allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something looks off after setup:&lt;/strong&gt; check that the values in &lt;code&gt;.env.local&lt;/code&gt; match your database exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you get stuck, tell me which step and which database you used. I'm a solo developer and I read every comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The next video in the series is Core vs Studio, the free self-hosted version compared with the hosted one. After that, we create your first model.&lt;/p&gt;

&lt;p&gt;Core is free and open source: &lt;a href="https://github.com/underpeaks/underpeaks-core?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=install-core" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which database are you planning to run it on?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>flutter</category>
      <category>nextjs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Self-Host a Headless CMS That Generates Flutter and Next.js Apps (in 5 Minutes)</title>
      <dc:creator>UnderPeaks</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:36:34 +0000</pubDate>
      <link>https://dev.to/under_peaks/how-to-self-host-a-headless-cms-that-generates-flutter-and-nextjs-apps-in-5-minutes-49n1</link>
      <guid>https://dev.to/under_peaks/how-to-self-host-a-headless-cms-that-generates-flutter-and-nextjs-apps-in-5-minutes-49n1</guid>
      <description>&lt;p&gt;If you've ever built the same app for web and mobile, you know the tax: two codebases, two sets of backend glue code, and one data model you keep in sync by hand. Change a field, update it in both places, miss one, ship a bug.&lt;/p&gt;

&lt;p&gt;Underpeaks Core is an open-source, self-hosted headless CMS built to remove that tax. You define your data model once, and it gives you a backend, a content editor, a REST API — and, if you want it, a working Flutter app and Next.js app compiled from that same model. No AI involved: same model in, same code out, every time.&lt;/p&gt;

&lt;p&gt;In this guide you'll go from zero to a running Core instance on your own machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js and npm&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;A database: Supabase, PostgreSQL, MySQL, MongoDB or Firebase — whichever you're most comfortable with. Have its connection details ready (host and credentials, or project URL and keys).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Get a free license key
&lt;/h2&gt;

&lt;p&gt;Core is free and open source (Apache 2.0), but it needs a license key — that's what lets the CLI generate code for your projects.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up for a free account at &lt;a href="https://underpeaks.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=install-core" rel="noopener noreferrer"&gt;underpeaks.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Generate a license key from your dashboard&lt;/li&gt;
&lt;li&gt;Keep the tab open — you'll paste the key during setup&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: Clone and install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/underpeaks/underpeaks-core.git
&lt;span class="nb"&gt;cd &lt;/span&gt;underpeaks-core
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Start Core
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt;. On a fresh install, you're sent straight to the installer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run the installer
&lt;/h2&gt;

&lt;p&gt;There are no environment variables to write by hand — the installer does it for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose your database&lt;/strong&gt; — one of the five supported options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enter your connection details&lt;/strong&gt; — the installer writes your &lt;code&gt;.env&lt;/code&gt; configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection test&lt;/strong&gt; — confirms Core can reach your database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table creation&lt;/strong&gt; — creates the system tables Core needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo content (optional)&lt;/strong&gt; — seeds an example project so you have something to explore straight away&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin account&lt;/strong&gt; — the email and password for your first user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License key&lt;/strong&gt; — paste the key from step 1&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When it finishes, you're redirected to sign in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Take the tour
&lt;/h2&gt;

&lt;p&gt;Sign in with your admin account and you'll land in the console:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared Models&lt;/td&gt;
&lt;td&gt;Define your data models — the schemas everything else is built on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages&lt;/td&gt;
&lt;td&gt;Build admin and client-facing pages from your models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Menu&lt;/td&gt;
&lt;td&gt;Control your sidebar navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigator&lt;/td&gt;
&lt;td&gt;Map how pages connect to each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme&lt;/td&gt;
&lt;td&gt;Colours and branding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Upload and manage media&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Users&lt;/td&gt;
&lt;td&gt;Manage who can access your instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settings&lt;/td&gt;
&lt;td&gt;Project name, license key, SMTP, API keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A good first exercise: create a model under &lt;strong&gt;Shared Models&lt;/strong&gt; (say, &lt;code&gt;Product&lt;/code&gt; with a title, price and image), then attach it to a page under &lt;strong&gt;Pages&lt;/strong&gt;. The admin views for it appear instantly — no extra configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Generate your apps
&lt;/h2&gt;

&lt;p&gt;Once a model is attached to a page, install the CLI and generate:&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; &lt;span class="nt"&gt;-g&lt;/span&gt; underpeaks-nxf
nxf login
nxf generate flutter   &lt;span class="c"&gt;# or: nxf generate nextjs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nxf login&lt;/code&gt; opens your browser to connect the CLI to your account; after that, &lt;code&gt;generate&lt;/code&gt; produces a real project you own — models, API client and auth flows included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Installer can't connect:&lt;/strong&gt; double-check your connection details, and make sure the database is reachable from where you're running Core (cloud databases often need your IP allowed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something looks off after setup:&lt;/strong&gt; check that your &lt;code&gt;.env.local&lt;/code&gt; values match your database exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Full code generation guide: &lt;a href="https://underpeaks.com/docs?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=install-core" rel="noopener noreferrer"&gt;docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source code: &lt;a href="https://github.com/underpeaks/underpeaks-core" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and something breaks or confuses you, I'd genuinely like to know — open an issue on GitHub or drop a comment below. Underpeaks is built by one developer, and feedback from real setups is the most useful thing I can get.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>flutter</category>
      <category>nextjs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why we built a five-database adapter layer (and what it cost in build time)</title>
      <dc:creator>UnderPeaks</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:21:25 +0000</pubDate>
      <link>https://dev.to/under_peaks/why-we-built-a-five-database-adapter-layer-and-what-it-cost-in-build-time-3990</link>
      <guid>https://dev.to/under_peaks/why-we-built-a-five-database-adapter-layer-and-what-it-cost-in-build-time-3990</guid>
      <description>&lt;p&gt;Every developer has had this moment. You pick a database on day one (Firebase because it's fast, Mongo because it's flexible, Postgres because it's "the right choice") and six months later you'd pick differently. By then, the database has leaked into everything.&lt;/p&gt;

&lt;p&gt;When I started building Underpeaks, a headless CMS that generates Flutter and Next.js apps from one data model, I made a decision that cost me far more time than I expected: it would run on five databases (Supabase, PostgreSQL, MySQL, MongoDB and Firebase) behind one interface.&lt;/p&gt;

&lt;p&gt;Here's why, how it works, and what it actually cost.&lt;/p&gt;

&lt;p&gt;Open source isn't the same as portable&lt;/p&gt;

&lt;p&gt;"Open source" is table stakes for a CMS now. But open source doesn't mean you can leave. Most tools bake in one database, and switching means a rewrite: queries, auth, migrations, the lot.&lt;/p&gt;

&lt;p&gt;I wanted the opposite. Your data model should be the source of truth, and the database should be a decision you can change your mind about.&lt;/p&gt;

&lt;p&gt;One interface, five engines&lt;/p&gt;

&lt;p&gt;Everything in Underpeaks Core talks to a single DBAdapter interface. Routes never import a database driver directly. They ask for the configured adapter:&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getConfiguredAdapter&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;dbConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nxf_users&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;Each adapter implements the same methods (create, read, readAll, update, delete, plus auth helpers) for its own engine. Swap the adapter, and the rest of the codebase doesn't know or care.&lt;/p&gt;

&lt;p&gt;That's the easy part to describe. The hard part is that five databases disagree about almost everything.&lt;/p&gt;

&lt;p&gt;Where the databases disagree&lt;/p&gt;

&lt;p&gt;Primary keys. SQL tables often use id. But a model called products might declare product_id as its primary key, and Firebase documents have IDs that aren't fields at all. Every update and delete has to know which column is the real key, so the interface takes it explicitly:&lt;/p&gt;

&lt;p&gt;ts&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;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product_id&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;Forget that last argument on a model whose key isn't literally id, and the update silently matches nothing. I learned that one the hard way.&lt;/p&gt;

&lt;p&gt;Concurrency. Firing off parallel queries with Promise.all is harmless with Firebase or Supabase's HTTP client. With a pooled Postgres or MySQL connection, it can exhaust connections or interleave in ways you don't expect. The SQL adapters now read sequentially. It's slower on paper and far more predictable in practice.&lt;/p&gt;

&lt;p&gt;Response shapes. One path returned { data }, another returned { records }. Generated apps expecting one shape silently got undefined from the other. Consistent envelopes aren't glamorous, but they're the difference between "it works" and "it works on my database."&lt;/p&gt;

&lt;p&gt;Schema. SQL wants real tables and columns. MongoDB and Firebase are happy with whatever you send. The adapter layer has to create physical tables where the engine needs them, and enforce structure where the engine won't.&lt;/p&gt;

&lt;p&gt;What it cost&lt;/p&gt;

&lt;p&gt;Honestly? Much more than I planned. Supporting one database would have taken about [2-3 weeks]. Five took [4-5 months].&lt;/p&gt;

&lt;p&gt;The cost isn't writing five adapters. It's that every feature now has five edge cases. Auth, file storage, pagination and filtering each work slightly differently per engine, and every bug has to be checked against all five. A fix that's correct for Postgres can be wrong for Firebase.&lt;/p&gt;

&lt;p&gt;The trade-offs, honestly&lt;/p&gt;

&lt;p&gt;A single-database tool can do things this approach can't:&lt;/p&gt;

&lt;p&gt;Use engine-specific features freely. Postgres full-text search, Mongo aggregation pipelines, Firebase realtime listeners. An adapter layer has to target what all five share, or handle each specially.&lt;br&gt;
Move faster. One engine means one set of edge cases.&lt;br&gt;
Tune performance deeply for that one database.&lt;/p&gt;

&lt;p&gt;If you know you'll only ever use Postgres, a Postgres-native tool is a perfectly good choice.&lt;/p&gt;

&lt;p&gt;Who it's for&lt;/p&gt;

&lt;p&gt;The adapter layer is for developers who:&lt;/p&gt;

&lt;p&gt;already run a database and don't want to migrate to use a CMS&lt;br&gt;
build for clients with different infrastructure&lt;br&gt;
want the option to change their mind later without a rewrite&lt;br&gt;
Database freedom as insurance&lt;/p&gt;

&lt;p&gt;I don't think of multi-database support as a feature you use every day. It's insurance. Most projects will never switch databases, but knowing you can changes how comfortably you commit.&lt;/p&gt;

&lt;p&gt;Underpeaks Core is open source and self-hosted. Pick your engine, and keep the option to change your mind.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://underpeaks.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=adapter-layer" rel="noopener noreferrer"&gt;Try Underpeaks Core&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github: &lt;a href="https://github.com/underpeaks/underpeaks-core" rel="noopener noreferrer"&gt;Underpeaks_core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>database</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why I Made the Data Model the Single Source of Truth for Both Mobile and Web</title>
      <dc:creator>UnderPeaks</dc:creator>
      <pubDate>Thu, 24 Sep 2026 08:25:14 +0000</pubDate>
      <link>https://dev.to/under_peaks/why-i-made-the-data-model-the-single-source-of-truth-for-both-mobile-and-web-11cf</link>
      <guid>https://dev.to/under_peaks/why-i-made-the-data-model-the-single-source-of-truth-for-both-mobile-and-web-11cf</guid>
      <description>&lt;p&gt;When you're building a product that needs both a mobile app and a web app, there's a decision point early on: do you build two frontends against one backend, or do you try to share more than that?&lt;/p&gt;

&lt;p&gt;Most teams end up with two frontends, one backend, and a growing list of small inconsistencies between them. A field gets added on mobile and forgotten on web. A validation rule changes in one place and not the other. Nothing catastrophic, just a slow accumulation of drift.&lt;/p&gt;

&lt;p&gt;I built Underpeaks around a different assumption: the schema shouldn't just define your backend, it should define your applications too.&lt;/p&gt;

&lt;h2&gt;
  
  
  How that plays out technically
&lt;/h2&gt;

&lt;p&gt;A model in Underpeaks isn't just a database table definition. It carries enough information, field types, UI hints, relationships, validation, to generate a working page in both a Flutter app and a Next.js app. Change the model, regenerate, both applications reflect the change.&lt;/p&gt;

&lt;p&gt;The generation isn't destructive either. Files you've customized are tracked via checksums and left untouched on regeneration. Only genuinely new pieces (a newly added field, say) get merged in. This matters a lot in practice, generated code you can't safely touch is generated code you'll eventually abandon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database layer
&lt;/h2&gt;

&lt;p&gt;Underpeaks runs on five database backends (Supabase, Postgres, MySQL, MongoDB, Firebase) through one adapter interface. This was a deliberate choice: teams shouldn't have to commit to a database vendor just to use a code generation tool. Same schema, same generated output, regardless of which one sits underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted by default philosophy
&lt;/h2&gt;

&lt;p&gt;Underpeaks Core is free, open source, and self-hosted. I built it this way first, before the hosted version, because I wanted the tool to work fully even for someone who never pays for anything. Underpeaks Studio (hosted) exists for teams that want managed infrastructure, not because the self-hosted option is deliberately limited.&lt;/p&gt;

&lt;p&gt;Launching on Product Hunt today: &lt;a href="https://www.producthunt.com/products/underpeaks?launch=underpeaks" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/underpeaks?launch=underpeaks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Genuinely interested in hearing how other people have approached this same problem, shared schemas, code generation, monorepo strategies, whatever's worked or not worked for you.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>saas</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>I got tired of building the same backend twice, so I built a tool that generates it for me</title>
      <dc:creator>UnderPeaks</dc:creator>
      <pubDate>Thu, 24 Sep 2026 08:03:07 +0000</pubDate>
      <link>https://dev.to/under_peaks/i-got-tired-of-building-the-same-backend-twice-so-i-built-a-tool-that-generates-it-for-me-1h28</link>
      <guid>https://dev.to/under_peaks/i-got-tired-of-building-the-same-backend-twice-so-i-built-a-tool-that-generates-it-for-me-1h28</guid>
      <description>&lt;p&gt;Every time I shipped a product that needed both a mobile app and a web app, I ended up doing the same thing: build the backend, build the Flutter app on top of it, then build a whole separate Next.js app on top of the same data, and keep both in sync by hand every time the schema changed.&lt;/p&gt;

&lt;p&gt;That got old fast. So I built Underpeaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;You define your data model once in the console, fields, types, relationships. From that one schema, Underpeaks generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real Flutter app, with your choice of Riverpod, Provider, or BLoC&lt;/li&gt;
&lt;li&gt;A real Next.js app, with your choice of Zustand, React Query, or Jotai&lt;/li&gt;
&lt;li&gt;A REST API&lt;/li&gt;
&lt;li&gt;An admin dashboard with KPIs and charts, if you want it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not scaffolding. Real, readable source code, using the state management pattern you already know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database question
&lt;/h2&gt;

&lt;p&gt;Underpeaks isn't tied to one database. Five adapters ship out of the box: Supabase, Postgres, MySQL, MongoDB, Firebase. Same interface regardless of which one you pick, so if you start on Supabase and later need to move, your schema and generated code come with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted, actually
&lt;/h2&gt;

&lt;p&gt;Underpeaks Core is free, open source, and runs on your own infrastructure. No calling home, no feature gates you didn't agree to. Underpeaks Studio is the hosted version if you'd rather not run it yourself.&lt;/p&gt;

&lt;p&gt;Launching on Product Hunt today, would love feedback from people who've hit the same problem: &lt;a href="https://www.producthunt.com/products/underpeaks?launch=underpeaks" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/underpeaks?launch=underpeaks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer anything in the comments, architecture, tradeoffs, what's still rough around the edges. It's a solo build, so there's plenty.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>nextjs</category>
      <category>saas</category>
      <category>lowcode</category>
    </item>
  </channel>
</rss>
