<?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: Lotus Tech Labs</title>
    <description>The latest articles on DEV Community by Lotus Tech Labs (@lotustechlabs).</description>
    <link>https://dev.to/lotustechlabs</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%2F4158492%2Fa238228f-0aee-49c8-a02b-e8dcedd7bbb5.png</url>
      <title>DEV Community: Lotus Tech Labs</title>
      <link>https://dev.to/lotustechlabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lotustechlabs"/>
    <language>en</language>
    <item>
      <title>Build vs buy: buy the commodity, build your edge</title>
      <dc:creator>Lotus Tech Labs</dc:creator>
      <pubDate>Fri, 02 Oct 2026 20:46:39 +0000</pubDate>
      <link>https://dev.to/lotustechlabs/build-vs-buy-buy-the-commodity-build-your-edge-4ia1</link>
      <guid>https://dev.to/lotustechlabs/build-vs-buy-buy-the-commodity-build-your-edge-4ia1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Buy anything your competitor can also buy. Build only the thing they choose you for.&lt;/strong&gt; That is the entire rule. Build or buy arguments run long because applying it honestly means admitting that your favourite internal project is a worse version of something you could rent this afternoon.&lt;/p&gt;

&lt;p&gt;We build custom software for a living, so weigh the next part accordingly: most of what a company runs should not be custom, and a good share of what gets built in-house is a CRM that could have been rented for the price of a fortnight of engineering time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commodity and edge
&lt;/h2&gt;

&lt;p&gt;A commodity is anything your competitor can buy on the same terms you can. A customer relationship manager is a commodity. So is payroll, email, error tracking, payments, and the database under all of it. Nobody has ever chosen a supplier because that supplier had written its own CRM.&lt;/p&gt;

&lt;p&gt;Your edge is the part of the product a customer would notice if it disappeared. It is usually narrower than people expect, and it is almost never the part that was fun to build.&lt;/p&gt;

&lt;p&gt;So when the question is whether to build something that already exists as a product, start by asking which of those two things it is. If a competitor can sign up for the same tool tomorrow, building it buys you nothing they cannot match in a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  One product, and every call we made on it
&lt;/h2&gt;

&lt;p&gt;We built a ticketing platform where event organisers onboard, create events, sell passes and scan them at the door. Here is what we bought, and the reason in each case was the same: a competitor could buy it too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payments.&lt;/strong&gt; Razorpay. Payments is a commodity to us and a compliance department to them. Building it means holding card data and inheriting an audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push notifications.&lt;/strong&gt; Firebase. There is no version of this we could write that is better than the free one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTP over SMS and WhatsApp.&lt;/strong&gt; Bought. Delivery to a phone network is somebody else’s relationship with carriers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database and cache.&lt;/strong&gt; Postgres on Aurora Serverless, and Valkey. Managed, because the alternative is becoming a database administrator by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling.&lt;/strong&gt; AWS auto-scaling. A scheduler is not a differentiator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here is the one thing we built: &lt;strong&gt;pass scanning that survives a venue with bad internet.&lt;/strong&gt; Scans queue on the device and reconcile when the connection returns, without letting the same pass through twice. No product sells that for this use case, and it is the difference between a queue moving and a queue stopping at the one moment the client cannot afford it.&lt;/p&gt;

&lt;p&gt;That is the shape of a healthy decision list. Five bought, one built, and the built one is the reason the product works at a real venue on a bad day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same rule inside an AI feature
&lt;/h2&gt;

&lt;p&gt;On another system we built image similarity search over a library of roughly five million photographs, answering in under 200 milliseconds for about a million monthly active users.&lt;/p&gt;

&lt;p&gt;We did not write a vector database. We used &lt;strong&gt;Qdrant&lt;/strong&gt;, because a vector store is a commodity and a very good one is free. What we built was the part that is not: the embedding pipeline, the index strategy, the reindexing and the backfill. A vector database is something you install. Making five million images answer in under 200 milliseconds is something you design, and it is where every hour of that project paid for itself.&lt;/p&gt;

&lt;p&gt;The same split holds for most &lt;a href="https://lotustechlabs.com/services/ai-ml-development" rel="noopener noreferrer"&gt;AI work&lt;/a&gt;: the model and the store are bought, and the retrieval, the evaluation and the failure cases are built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two costs people get wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Buying:&lt;/strong&gt; the sticker price is not the cost. The cost is per-seat creep as you hire, and the exit. Before you adopt anything your workflows will live inside, ask what leaving costs. Your data will be exportable, because every vendor says so. Your integrations, automations and the habits of the people using it will not be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building:&lt;/strong&gt; the build is the cheap part. A thing you build is maintained for as long as the company exists. Somebody is on call for it. Somebody upgrades the dependency with the advisory against it. The person who wrote it leaves, and the next person spends a month learning what it does before they can change it safely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A build is not a project. It is a hire you have not made yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the number missing from most build-or-buy spreadsheets. People compare a one-off build estimate against a recurring subscription, which is comparing the wrong two things. Compare the subscription against the build plus a decade of somebody owning it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard case: when a bought thing becomes your edge
&lt;/h2&gt;

&lt;p&gt;The rule is easy at the extremes and difficult in the middle, and the difficult middle is real. You buy something at twenty people because it is obviously a commodity. At a hundred people the workflow built around it has quietly become how the company makes money.&lt;/p&gt;

&lt;p&gt;One marketplace we built for had the problem every marketplace has: vendors will not write their own listings, and an empty listing sells nothing. The answer was automation that turns a short prompt into the description, the FAQs and the imagery.&lt;/p&gt;

&lt;p&gt;We did not build a workflow engine. We used &lt;strong&gt;n8n&lt;/strong&gt; and built the prompt pipeline and the data mapping on top of it. The orchestration is a commodity. What the prompts do with that marketplace’s own data is not, and it is the part a competitor cannot copy by buying the same tool.&lt;/p&gt;

&lt;p&gt;That is usually the answer in the difficult middle. Not build or buy, but buy the engine and build the part that is yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  We applied this to ourselves this week
&lt;/h2&gt;

&lt;p&gt;Our own contact address needed a real mailbox, somewhere we could reply from rather than just forward to. Three options: stitch something together free, pay a small amount for one provider, or pay a bit more for another.&lt;/p&gt;

&lt;p&gt;The rule settled it in about a minute. Is email deliverability our edge? No. Can a competitor buy exactly the same thing? Yes, in four minutes, with a credit card. So we buy it, and we spend the saved time on something a client is paying for.&lt;/p&gt;

&lt;p&gt;The useful part is the temptation we had to turn down. We are engineers. We &lt;strong&gt;could&lt;/strong&gt; run a mail server, and some of us would enjoy it. That is exactly the trap: being capable of building something is not a reason to build it. The question is never whether you can.&lt;/p&gt;

&lt;p&gt;It helps that the downside is lopsided. Getting email wrong means an enquiry lands in a spam folder, and one lost enquiry costs more than several years of the subscription. When the failure is that asymmetric, buy the thing with the best track record and stop thinking about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions that settle it on a call
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If we buy this, does any customer ever notice?&lt;/strong&gt; If the honest answer is no, buy it. Internal tooling is not where you earn a reputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If this breaks at 3am in two years, who fixes it?&lt;/strong&gt; Name the person. If they are not on the payroll yet, you are costing a hire, not a project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What does leaving cost?&lt;/strong&gt; Not exporting the data. Rebuilding every workflow, integration and habit that grew around it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a thing is a commodity, cheap to leave and invisible to customers, buy it today and stop discussing it. If it is the reason somebody picks you, build it properly and own it. Almost everything is the first kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  This rule costs us work, and we apply it anyway
&lt;/h2&gt;

&lt;p&gt;We turn down work that amounts to rebuilding something a client could rent. That is not generosity. An engineering budget spent on an internal CRM is a budget not spent on the thing that wins the next customer, and a client who runs out of money reconstructing commodity software does not come back for the project that mattered.&lt;/p&gt;

&lt;p&gt;If you are weighing one of these, &lt;a href="https://lotustechlabs.com/contact" rel="noopener noreferrer"&gt;tell us what you are building&lt;/a&gt; and you will get an honest read on which side of the line it falls, including the times the answer is to buy it and not call us.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://lotustechlabs.com/blog/build-vs-buy-software" rel="noopener noreferrer"&gt;Lotus Tech Labs blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>startup</category>
      <category>management</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Your Next.js build should not need your database</title>
      <dc:creator>Lotus Tech Labs</dc:creator>
      <pubDate>Fri, 02 Oct 2026 20:43:37 +0000</pubDate>
      <link>https://dev.to/lotustechlabs/your-nextjs-build-should-not-need-your-database-3o85</link>
      <guid>https://dev.to/lotustechlabs/your-nextjs-build-should-not-need-your-database-3o85</guid>
      <description>&lt;p&gt;If a page reads from your database, render it &lt;strong&gt;on demand&lt;/strong&gt;, not during the build. Prerendering it means your build container needs a route to Postgres, and on self-hosted infrastructure that route is the most host-specific thing you own. We learned this by paying for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we were doing
&lt;/h2&gt;

&lt;p&gt;This site runs Next.js with Payload CMS in-process, on Postgres, in Docker, on a single VPS. Six service pages moved into the CMS. They were dynamic segments with a &lt;code&gt;generateStaticParams&lt;/code&gt; that queried Payload for every slug, so all six were prerendered into HTML at build time. That is the configuration every tutorial shows you, and on Vercel it works, because the build runs inside the same network as the managed database.&lt;/p&gt;

&lt;p&gt;Ours does not. The build runs in a Docker build container, and a build container &lt;strong&gt;does not join the overlay network&lt;/strong&gt; the services talk over. So the hostname the application resolves at runtime does not resolve at build time, and the build fails with a DNS error rather than anything that mentions networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not work
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to make the database reachable from the build: publish Postgres on the host and point the build at the Docker bridge gateway. We did that. It took &lt;strong&gt;five deploys&lt;/strong&gt; to find the address.&lt;/p&gt;

&lt;p&gt;Every answer online says the gateway is &lt;code&gt;172.17.0.1&lt;/code&gt;. On this box, inside the builder, it is not. We stopped guessing and made the build print its own default route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;ip route | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/default/ {print "gateway is", $3}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only reliable way to learn a build container’s gateway is to ask it.&lt;/p&gt;

&lt;p&gt;It answered &lt;code&gt;172.16.0.1&lt;/code&gt; - specific to this host’s builder, not a Docker default, and not something any amount of reading would have told us.&lt;/p&gt;

&lt;p&gt;That got the build passing, and left two bills. The database had to be &lt;strong&gt;published to the host&lt;/strong&gt; to make it reachable at all, reversing a decision whose code comment read "publishing 5432 on a public IP is how Postgres instances end up in ransom notes". And the build now depended on one machine’s network layout, so rebuilding on EC2 would mean rediscovering that address, with a failed build as the only clue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The documented workaround was worse
&lt;/h2&gt;

&lt;p&gt;Payload documents a build mode for exactly this situation, &lt;code&gt;next build --experimental-build-mode compile&lt;/code&gt;, which skips the data-fetching phase. It also stops inlining &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; environment variables.&lt;/p&gt;

&lt;p&gt;On this site one of those values decides whether the response carries &lt;code&gt;X-Robots-Tag: noindex&lt;/code&gt;. With it absent, a production build serves a correct-looking site that quietly tells every crawler to go away. The failure is invisible until organic traffic disappears weeks later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A workaround that trades a loud build failure for a silent production one is not a workaround.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What we do instead
&lt;/h2&gt;

&lt;p&gt;The dynamic segment stays, and its &lt;code&gt;generateStaticParams&lt;/code&gt; returns an empty list. Deliberately - not as a fallback.&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;// app/(frontend)/services/[slug]/page.tsx&lt;/span&gt;

&lt;span class="c1"&gt;// Empty on purpose. Returning real slugs would query Payload, which would open a&lt;/span&gt;
&lt;span class="c1"&gt;// Postgres connection during `next build` and tie the build to one host's network.&lt;/span&gt;
&lt;span class="c1"&gt;// Unknown slugs still 404, because `dynamicParams` stays at its default.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is prerendered. The first request for a real slug renders the page in about 200ms and Next caches it; every request after that is a cache hit. When an editor publishes a change, the collection’s &lt;code&gt;afterChange&lt;/code&gt; hook calls &lt;code&gt;revalidatePath&lt;/code&gt; and the page updates without a deploy.&lt;/p&gt;

&lt;p&gt;Postgres goes back to publishing nothing. The build secrets and the gateway detection are deleted. &lt;strong&gt;Nothing host-specific remains&lt;/strong&gt;: moving to another server means restoring the database and deploying the containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that look like fixes and are not
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;export const revalidate&lt;/code&gt; does not keep the build away from the database
&lt;/h3&gt;

&lt;p&gt;Setting a revalidation period on a page does not stop Next prerendering it. It &lt;strong&gt;still renders the initial payload at build time&lt;/strong&gt; and then refreshes on a timer afterwards. If that render reads your database, your build reads your database. The route has to be dynamic, not merely revalidating.&lt;/p&gt;

&lt;h3&gt;
  
  
  A page with no route parameters has no &lt;code&gt;generateStaticParams&lt;/code&gt; to empty
&lt;/h3&gt;

&lt;p&gt;We hit this the moment we added a blog index. There is no &lt;code&gt;[slug]&lt;/code&gt; on &lt;code&gt;/blog&lt;/code&gt;, so there is nothing to return an empty list from, and Next prerendered it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error occurred prerendering page "/blog".
cannot connect to Postgres: getaddrinfo ENOTFOUND base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix for a parameter-less page is &lt;code&gt;export const dynamic = 'force-dynamic'&lt;/code&gt;. Same for anything Next evaluates at build: our sitemap and RSS feed are route handlers marked dynamic with their own cache headers, rather than the typed metadata files, precisely because they list posts from the CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, stated plainly
&lt;/h2&gt;

&lt;p&gt;This is a trade, not a free win, and the limits are worth naming because they decide whether it suits you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first request after a deploy is slower.&lt;/strong&gt; About 200ms here, then cached. Fine for content pages; measure it before doing this to something latency-sensitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A database outage with a cold cache breaks those pages.&lt;/strong&gt; Prerendered HTML would have survived it. We accept that: the pages that matter most are fully static and unaffected, and a health endpoint reports the database so the outage is visible rather than inferred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages that are no longer files in the build output drop out of any CI check that inspects those files.&lt;/strong&gt; Ours asserted one social-card image per page by reading the built HTML. That check had to be rewritten.&lt;/li&gt;
&lt;li&gt;If you are on a managed database with a hostname reachable from anywhere, you can have both. We looked at that and chose not to add a monthly bill and an external dependency to a site this size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also decided against a fallback: if Postgres is unreachable the page fails rather than serving the copy committed in the repository. Visibly broken beats silently stale, because stale wording is the kind of thing nobody notices for a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule we kept
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If it renders from a database, it renders on demand.&lt;/strong&gt; Written down, with the reasoning, as an architecture decision record, because the next person to see an empty &lt;code&gt;generateStaticParams&lt;/code&gt; will read it as an oversight and "fix" it. That one line is worth more than the fix itself: the build stopped being the thing that knows where our database lives, and that is what makes the stack portable.&lt;/p&gt;

&lt;p&gt;If you hit the same wall, the short version is: do not teach your build how to reach your database. Teach your pages to render later. And if you are working through this on your own stack, &lt;a href="https://lotustechlabs.com/contact" rel="noopener noreferrer"&gt;tell us what broke&lt;/a&gt; - the failure modes above cost us real deploys, and the list is almost certainly not complete.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://lotustechlabs.com/blog/your-nextjs-build-should-not-need-your-database" rel="noopener noreferrer"&gt;Lotus Tech Labs blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>docker</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
