<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged postgresql</title>
<link>https://coderlegion.com/tag/postgresql</link>
<description>Powered by Question2Answer</description>
<item>
<title>Database Migrations: Can Your Previous Release Still Run?</title>
<link>https://coderlegion.com/31693/database-migrations-can-your-previous-release-still-run</link>
<description>&lt;p&gt;A deployment can roll back cleanly while the database stays incompatible with the old application. Before shipping a schema change, check whether the previous release can still read and write the data that the new release creates.&lt;/p&gt;
&lt;p&gt;Consider a fictional account service. Version 1 reads a column called &lt;code&gt;display_name&lt;/code&gt;. Version 2 expects &lt;code&gt;public_name&lt;/code&gt;. Renaming the column and deploying the new code together looks tidy, until an old worker, a rolling-deployment instance, or an emergency rollback still asks for &lt;code&gt;display_name&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The migration succeeded. The service can still fail.&lt;/p&gt;
&lt;h3 id=&quot;separate-compatibility-from-cleanup&quot;&gt;Separate compatibility from cleanup&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/bliki/ParallelChange.html&quot;&gt;Danilo Sato’s explanation of parallel change&lt;/a&gt; describes three stages: expand, migrate, and contract. The useful idea is to introduce the new interface while the old one remains available, move consumers, then remove the old interface.&lt;/p&gt;
&lt;p&gt;For a database rollout, turn that idea into explicit release gates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expand:&lt;/strong&gt; introduce the new storage shape without removing what the currently deployed application needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate:&lt;/strong&gt; move reads and writes deliberately, checking compatibility during the transition.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contract:&lt;/strong&gt; remove the old shape only after its consumers and the intended rollback window are accounted for.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These stages are a planning model, not a guarantee of zero downtime. The exact migration still depends on the database, data volume, application behavior, and operational limits.&lt;/p&gt;
&lt;h3 id=&quot;work-through-the-awkward-middle&quot;&gt;Work through the awkward middle&lt;/h3&gt;
&lt;p&gt;In the account-service example, adding &lt;code&gt;public_name&lt;/code&gt; is only the beginning. Before copying values, decide which field is authoritative at each stage.&lt;/p&gt;
&lt;p&gt;A possible transition is to deploy compatibility code that continues serving old readers while maintaining both values. If both columns live in the same row, update them in the same database transaction where practical. That still leaves an important question: can any old application instance or background job write only the original column?&lt;/p&gt;
&lt;p&gt;Do not switch reads to the new column while those writers can silently make it stale. Either finish upgrading writers first or design a deliberate synchronization mechanism. A fallback read helps with missing values; it does not detect an outdated non-empty value.&lt;/p&gt;
&lt;p&gt;Backfills need the same care. A naive copy can overwrite a newer value if it races with a live update. Define an update condition, concurrency strategy, and restart behavior before running the job. Record progress so an interrupted backfill can resume without blindly replaying the entire table.&lt;/p&gt;
&lt;h3 id=&quot;test-the-rollback-with-new-data&quot;&gt;Test the rollback with new data&lt;/h3&gt;
&lt;p&gt;A useful rehearsal includes more than deploying version 2 successfully:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with representative data, including empty and unusual values.&lt;/li&gt;
&lt;li&gt;Apply the expansion and deploy the compatibility release.&lt;/li&gt;
&lt;li&gt;Run the backfill alongside representative writes.&lt;/li&gt;
&lt;li&gt;Create and edit records using the new release.&lt;/li&gt;
&lt;li&gt;Restore the application version you actually intend to roll back to.&lt;/li&gt;
&lt;li&gt;Verify that it can read and update those new records correctly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Include background workers, scheduled jobs, exports, and direct database consumers in that exercise. A healthy web endpoint does not prove that every writer is compatible.&lt;/p&gt;
&lt;p&gt;Use synthetic or appropriately protected test data. This checklist proposes evidence to collect; it does not claim that a production migration has been tested.&lt;/p&gt;
&lt;h3 id=&quot;a-compatible-change-can-still-block-traffic&quot;&gt;A compatible change can still block traffic&lt;/h3&gt;
&lt;p&gt;In PostgreSQL, &lt;code&gt;ALTER TABLE&lt;/code&gt; generally acquires an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock unless the documentation specifies otherwise. Compatibility at the application level does not eliminate lock waits.&lt;/p&gt;
&lt;p&gt;PostgreSQL also supports separating certain constraint additions from validation with &lt;code&gt;NOT VALID&lt;/code&gt; and &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt;. The permitted constraint types and locking details matter. Check the documentation for your deployed version rather than applying the technique to every schema operation. See the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/sql-altertable.html&quot;&gt;PostgreSQL ALTER TABLE reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Before execution, define acceptable lock wait, query latency, replication lag, and backfill load. Decide who can stop the rollout and what the safe next step is if a threshold is crossed.&lt;/p&gt;
&lt;h3 id=&quot;make-cleanup-its-own-decision&quot;&gt;Make cleanup its own decision&lt;/h3&gt;
&lt;p&gt;Keep a short release note that answers four questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which application versions can run against this schema?&lt;/li&gt;
&lt;li&gt;Which data changes can the previous version understand?&lt;/li&gt;
&lt;li&gt;What evidence allows the old column or interface to be removed?&lt;/li&gt;
&lt;li&gt;After removal, does recovery mean application rollback, a forward fix, or data restoration?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Give the cleanup an owner. Leaving parallel paths indefinitely creates its own maintenance cost, but removing them too early destroys the compatibility you built.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The practical takeaway:&lt;/strong&gt; rehearse rollback against data written by the new release before treating it as a recovery plan.&lt;/p&gt;
&lt;p&gt;What is the most easily missed database consumer in your deployment process?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Waqas Ahmad | Software Engineer | Content Creator&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31693/database-migrations-can-your-previous-release-still-run</guid>
<pubDate>Mon, 19 Oct 2026 07:45:55 +0000</pubDate>
</item>
<item>
<title>The Reconnect Bug Where Marking a User Online Erased the Data I Needed for Replay</title>
<link>https://coderlegion.com/31164/the-reconnect-bug-where-marking-a-user-online-erased-the-data-i-needed-for-replay</link>
<description>&lt;p&gt;While testing a reconnect system for a deliverer-facing WebSocket gateway: when someone reconnects after losing their connection, they should get a snapshot of their current job plus any events they missed while offline. Two pieces, both of which I was confident were correct on their own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Marking presence:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async markOnline(userId: number) {
  await this.prisma.user.update({
    where: { id: userId },
    data: { isOnline: true, lastSeenAt: new Date() },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Computing what was missed:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async replayDelivererEvents(delivererId: number) {
  const deliverer = await this.prisma.delivererProfile.findUnique({
    where: { id: delivererId },
    select: { user: { select: { lastSeenAt: true } }, activeJobId: true },
  });
  // ...fetch events since deliverer.user.lastSeenAt
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;And the connection handler that used both:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async handleConnection(client: Socket) {
  const deliverer = await this.getDelivererByUserId(payload.sub);

  await this.sessionService.markOnline(payload.sub);           // step 1
  const missedEvents = await this.replayService.replayForDeliverer(deliverer.id); // step 2

  client.emit('events.replay', missedEvents);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tested it: disconnect, trigger a job assignment while offline, reconnect. &lt;strong&gt;events.replay&lt;/strong&gt; came back empty, every time. The event genuinely happened. The query genuinely should have found it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Walk through the actual timeline. A deliverer disconnects — markOffline correctly stamps lastSeenAt with the disconnect time. Later, they reconnect. handleConnection fires, and the first thing it does is call markOnline, which immediately overwrites lastSeenAt to right now. Only after that does it call replayForDeliverer, which reads lastSeenAt to compute &quot;what happened since they were last seen&quot;  except by then lastSeenAt no longer holds the disconnect time. It holds the current moment, because the line above just wrote over it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;getEventsSince({ since: lastSeenAt })&lt;/strong&gt; effectively became since: right now. The replay feature couldn't fail more quietly — no error, no exception, just a correctly-shaped empty array every single time.&lt;/p&gt;
&lt;p&gt;What makes this bug worth writing about isn't the fix — it's that neither piece of code was wrong in isolation. Marking someone online should update lastSeenAt. Replay should read lastSeenAt. The bug only exists in the order they ran in, and that kind of bug doesn't show up by staring at either function — only by tracing the actual sequence of operations across the whole request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Read before you overwrite:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async handleConnection(client: Socket) {
  const deliverer = await this.getDelivererByUserId(payload.sub);

  // Replay FIRST, using the lastSeenAt from before this connection
  const missedEvents = await this.replayService.replayForDeliverer(deliverer.id);

  // THEN mark online, which overwrites lastSeenAt for next time
  await this.sessionService.markOnline(payload.sub);

  client.emit('events.replay', missedEvents);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No schema change needed. Reordering two already-correct operations was the entire fix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The part worth remembering&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One field, two different jobs — &quot;when did we last hear a heartbeat&quot; and &quot;what's the anchor point for replay&quot; happened to share the same column, and that's exactly what made the collision possible. The one-line reorder fixes today's bug, but it leaves the deeper coupling in place: anything else that later wants to read lastSeenAt for a different purpose can walk into the same trap. The more robust version splits them. a dedicated &lt;strong&gt;lastDisconnectedAt&lt;/strong&gt;, set only on disconnect, read only by replay so two unrelated concerns stop sharing state that one of them mutates as a side effect. I haven't built that yet. It's on the list specifically because I know why it would help, not just that it might.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31164/the-reconnect-bug-where-marking-a-user-online-erased-the-data-i-needed-for-replay</guid>
<pubDate>Thu, 15 Oct 2026 07:00:41 +0000</pubDate>
</item>
<item>
<title>Calling a Service Inside prisma.$transaction Doesn't Make It Atomic — Here's the Bug That Taught Me</title>
<link>https://coderlegion.com/30791/calling-service-inside-prisma-transaction-doesnt-make-atomic-heres-the-bug-that-taught</link>
<description>&lt;p&gt;I was fixing a real bug in a delivery backend I'm building: a customer's order could get created in Postgres even when something downstream failed, leaving an order with no event history behind it. The fix looked simple. Wrap the whole thing in &lt;br&gt;
&lt;strong&gt;prisma.$transaction:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const order = await this.prisma.$transaction(async (tx) =&amp;gt; {
  const newOrder = await tx.order.create({ data: { ... } });

  await this.eventLogService.recordEvent({
    entityType: 'ORDER',
    entityId: newOrder.id,
    eventType: EVENTS.ORDER.CREATED,
    payload: { orderId: newOrder.id, status: OrderStatus.PENDING_VENDOR_CONFIRMATION },
  });

  return newOrder;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ran it, broke the event-logging step on purpose to test the rollback, and the order still got committed. The transaction did nothing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;$transaction(async (tx) =&amp;gt; {...})&lt;/strong&gt; opens one database connection and starts a transaction on it. The tx object you get inside the callback is bound to that specific connection. Every query run through tx participates in the same transaction.&lt;/p&gt;
&lt;p&gt;My &lt;strong&gt;EventLogService.recordEvent&lt;/strong&gt; method didn't use &lt;strong&gt;tx&lt;/strong&gt;. It used &lt;strong&gt;this.prisma&lt;/strong&gt; — its own injected Prisma client, a completely different connection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async recordEvent(params) {
  const event = await this.prisma.eventLog.create({ ... }); // not tx — its own connection
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nesting the call inside &lt;strong&gt;$transaction&lt;/strong&gt; changes nothing here. Prisma has no way to know &quot;we're inside a transaction right now, please route all queries through it.&quot; The only thing that determines whether a query participates in a transaction is which client object actually executed it. Since &lt;strong&gt;recordEvent&lt;/strong&gt; ran on its own connection, its insert committed instantly and independently, with no awareness that anything else was &quot;inside&quot; a transaction around it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;br&gt;
Every method that needs to be transaction-capable has to accept an optional transaction client and fall back to the default when none is passed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async recordEvent(params, tx?: Prisma.TransactionClient) {
  const client = tx ?? this.prisma;
  const event = await client.eventLog.create({ data: params });
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the caller passes its own &lt;strong&gt;tx&lt;/strong&gt; through explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;await this.eventLogService.recordEvent(params, tx);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;strong&gt;recordEvent's&lt;/strong&gt; insert runs on the same connection, inside the same transaction, as everything else. If anything later in that transaction throws, Postgres rolls back the whole connection's uncommitted work — including this insert — because it genuinely was part of the same unit of work, not just nested inside it syntactically.&lt;/p&gt;
&lt;p&gt;I retested the same failure scenario. This time, the order didn't get created either. Rollback worked as a single unit, order and event together.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The part worth remembering&lt;/strong&gt;&lt;br&gt;
Code structure and transaction membership are two different things. A function call being lexically inside a &lt;strong&gt;$transaction&lt;/strong&gt; block tells you nothing about which database connection it actually used. If you're composing transactional logic across services in Prisma — or any ORM with a similar explicit-client model — every service in that chain needs to accept and thread through the transaction client deliberately. There's no implicit &quot;ambient transaction&quot; you can rely on.&lt;/p&gt;
&lt;p&gt;The honest cost: this makes service signatures a bit noisier — an extra optional parameter on every method that might need to participate in a caller's transaction. I think that's a fair trade for not silently losing atomicity on exactly the kind of multi-step write where you'd assume you had it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30791/calling-service-inside-prisma-transaction-doesnt-make-atomic-heres-the-bug-that-taught</guid>
<pubDate>Mon, 12 Oct 2026 09:22:43 +0000</pubDate>
</item>
<item>
<title>From React to PostgreSQL: Understanding the Full Stack</title>
<link>https://coderlegion.com/30077/from-react-to-postgresql-understanding-the-full-stack</link>
<description>&lt;p&gt;Modern web applications are no longer built with a single technology. A production application usually involves multiple layers working together: a frontend that users interact with, a backend that processes requests, APIs that connect different components, and a database that stores information.&lt;/p&gt;
&lt;p&gt;One of the most popular combinations for building modern full stack applications is &lt;strong&gt;React  Node.js/Express + PostgreSQL&lt;/strong&gt;. React provides an interactive user interface, the backend handles business logic and communication, and PostgreSQL provides reliable and structured data storage.&lt;/p&gt;
&lt;p&gt;Understanding how these technologies communicate is much more valuable than simply learning each technology separately. A developer who understands the entire request lifecycle can build applications that are easier to maintain, debug, secure, and scale.&lt;/p&gt;
&lt;p&gt;This article explores the complete journey from React in the browser to PostgreSQL in the database and explains how all the pieces fit together.&lt;/p&gt;
&lt;h3 id=&quot;1-what-does-full-stack-actually-mean-&quot;&gt;1. What Does &quot;Full Stack&quot; Actually Mean?&lt;/h3&gt;
&lt;p&gt;The term &lt;strong&gt;full stack development&lt;/strong&gt; refers to working across multiple layers of a web application.&lt;/p&gt;
&lt;p&gt;A typical application can be divided into four major areas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**Frontend What users see and interact with&lt;/li&gt;
&lt;li&gt;**Backend Server-side logic and application processing&lt;/li&gt;
&lt;li&gt;**API  Communication between frontend and backend&lt;/li&gt;
&lt;li&gt;**Database  Persistent storage of application data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simplified architecture looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User
  ↓
React Frontend
  ↓
HTTP Request
  ↓
Backend / API
  ↓
Business Logic
  ↓
PostgreSQL Database
  ↓
Backend Response
  ↓
React UI Update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, imagine a task management application.&lt;/p&gt;
&lt;p&gt;A user opens the application and sees a list of tasks. When they click &lt;strong&gt;&quot;Add Task&quot;&lt;/strong&gt;, React collects the task information and sends it to the backend.&lt;/p&gt;
&lt;p&gt;The backend validates the information, processes the request, and sends an SQL query to PostgreSQL.&lt;/p&gt;
&lt;p&gt;PostgreSQL stores the task and returns the result.&lt;/p&gt;
&lt;p&gt;The backend then sends a response to React, and React updates the interface.&lt;/p&gt;
&lt;p&gt;The entire process may happen in milliseconds.&lt;/p&gt;
&lt;p&gt;Understanding this complete flow is the foundation of full-stack development.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-react-the-frontend-layer&quot;&gt;2. React: The Frontend Layer&lt;/h2&gt;
&lt;p&gt;React is a JavaScript library for building user interfaces.&lt;/p&gt;
&lt;p&gt;Instead of creating one large HTML page, React allows developers to divide an application into reusable components.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;function TaskCard({ task }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h3&amp;gt;{task.title}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;{task.description}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A larger application might contain components such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;App
├── Navbar
├── Sidebar
├── Dashboard
├── TaskList
│   └── TaskCard
└── Footer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component-based architecture makes large applications easier to manage.&lt;/p&gt;
&lt;h3 id=&quot;react-state&quot;&gt;React State&lt;/h3&gt;
&lt;p&gt;React applications frequently need to manage changing information.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const [tasks, setTasks] = useState([]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;tasks&lt;/code&gt; variable contains the current data, while &lt;code&gt;setTasks()&lt;/code&gt; updates it.&lt;/p&gt;
&lt;p&gt;When the state changes, React re-renders the relevant part of the interface.&lt;/p&gt;
&lt;p&gt;This makes React particularly useful for interactive applications.&lt;/p&gt;
&lt;h3 id=&quot;fetching-backend-data&quot;&gt;Fetching Backend Data&lt;/h3&gt;
&lt;p&gt;React usually communicates with a backend through HTTP requests.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;fetch(&quot;https://example.com/api/tasks&quot;)
  .then(response =&amp;gt; response.json())
  .then(data =&amp;gt; setTasks(data));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modern applications may also use libraries such as Axios or tools provided by frameworks.&lt;/p&gt;
&lt;p&gt;The important concept is that React itself normally does not communicate directly with PostgreSQL.&lt;/p&gt;
&lt;p&gt;Instead, it communicates with the backend.&lt;/p&gt;
&lt;p&gt;That separation is extremely important.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-why-react-should-not-connect-directly-to-postgresql&quot;&gt;3. Why React Should Not Connect Directly to PostgreSQL&lt;/h2&gt;
&lt;p&gt;A beginner might wonder:&lt;/p&gt;
&lt;p&gt;&quot;If React can make network requests, why not connect React directly to PostgreSQL?&quot;&lt;/p&gt;
&lt;p&gt;The answer is security and architecture.&lt;/p&gt;
&lt;p&gt;A browser application runs on the user's device. If database credentials were placed inside React code, anyone could potentially inspect them.&lt;/p&gt;
&lt;p&gt;For example, exposing something like this would be extremely dangerous:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const databasePassword = &quot;my-secret-password&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frontend code is not a secure location for database credentials.&lt;/p&gt;
&lt;p&gt;Instead, the architecture should look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React
   ↓
Backend API
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backend protects the database credentials and controls what users are allowed to access.&lt;/p&gt;
&lt;p&gt;This also allows the backend to perform validation, authentication, authorization, business logic, and database operations.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-the-backend-where-the-application-thinks&quot;&gt;4. The Backend: Where the Application Thinks&lt;/h2&gt;
&lt;p&gt;The backend is responsible for processing requests from the frontend.&lt;/p&gt;
&lt;p&gt;A popular JavaScript backend combination is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Express.js&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Node.js allows JavaScript to run outside the browser.&lt;/p&gt;
&lt;p&gt;Express provides tools for building APIs and handling HTTP requests.&lt;/p&gt;
&lt;p&gt;A simple Express route might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;app.get(&quot;/api/tasks&quot;, async (req, res) =&amp;gt; {
  const result = await db.query(&quot;SELECT * FROM tasks&quot;);

  res.json(result.rows);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When React requests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET /api/tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the backend receives the request.&lt;/p&gt;
&lt;p&gt;It queries PostgreSQL and returns the database results as JSON.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-understanding-apis&quot;&gt;5. Understanding APIs&lt;/h2&gt;
&lt;p&gt;An API acts as a communication layer between different parts of an application.&lt;/p&gt;
&lt;p&gt;For a task application, we might have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET    /api/tasks
GET    /api/tasks/:id
POST   /api/tasks
PUT    /api/tasks/:id
DELETE /api/tasks/:id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These endpoints represent different operations.&lt;/p&gt;
&lt;h4 id=&quot;get&quot;&gt;GET&lt;/h4&gt;
&lt;p&gt;Used to retrieve information.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET /api/tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;post&quot;&gt;POST&lt;/h4&gt;
&lt;p&gt;Used to create new information.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POST /api/tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;put-or-patch&quot;&gt;PUT or PATCH&lt;/h4&gt;
&lt;p&gt;Used to update information.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PATCH /api/tasks/25
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;delete&quot;&gt;DELETE&lt;/h4&gt;
&lt;p&gt;Used to remove information.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DELETE /api/tasks/25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern is commonly associated with REST APIs.&lt;/p&gt;
&lt;p&gt;A well-designed API gives the frontend a predictable way to communicate with the backend.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-postgresql-the-data-layer&quot;&gt;6. PostgreSQL: The Data Layer&lt;/h2&gt;
&lt;p&gt;PostgreSQL is a powerful open-source relational database system.&lt;/p&gt;
&lt;p&gt;It stores information in tables containing rows and columns.&lt;/p&gt;
&lt;p&gt;For example, a &lt;code&gt;tasks&lt;/code&gt; table could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;tasks
------------------------------------------------
id | title          | completed | created_at
------------------------------------------------
1  | Learn React    | false     | ...
2  | Build API      | true      | ...
3  | Learn PostgreSQL| false    | ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each row represents a record.&lt;/p&gt;
&lt;p&gt;Each column represents a property of that record.&lt;/p&gt;
&lt;p&gt;A PostgreSQL table might be created using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL provides powerful features for data integrity, relationships, transactions, indexing, querying, and more.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-sql-how-the-backend-talks-to-postgresql&quot;&gt;7. SQL: How the Backend Talks to PostgreSQL&lt;/h2&gt;
&lt;p&gt;SQL stands for Structured Query Language.&lt;/p&gt;
&lt;p&gt;Backend applications use SQL to interact with relational databases.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM tasks;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;returns tasks.&lt;/p&gt;
&lt;p&gt;To create a task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO tasks (title, description)
VALUES ('Learn Full Stack Development', 'Study React, APIs and PostgreSQL');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To update a task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;UPDATE tasks
SET completed = TRUE
WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To delete a task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;DELETE FROM tasks
WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These operations are the foundation of database-driven applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-the-complete-request-lifecycle&quot;&gt;8. The Complete Request Lifecycle&lt;/h2&gt;
&lt;p&gt;Let's follow a real request from React to PostgreSQL.&lt;/p&gt;
&lt;p&gt;Imagine a user submits this form:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Title: Learn PostgreSQL
Description: Understand relational databases
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-1-react-collects-the-data&quot;&gt;Step 1: React Collects the Data&lt;/h4&gt;
&lt;p&gt;React stores the form values in state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const [title, setTitle] = useState(&quot;&quot;);
const [description, setDescription] = useState(&quot;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the user submits the form, React creates an HTTP request.&lt;/p&gt;
&lt;h4 id=&quot;step-2-react-sends-the-request&quot;&gt;Step 2: React Sends the Request&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;fetch(&quot;/api/tasks&quot;, {
  method: &quot;POST&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;
  },
  body: JSON.stringify({
    title,
    description
  })
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser sends the request to the backend.&lt;/p&gt;
&lt;h4 id=&quot;step-3-express-receives-it&quot;&gt;Step 3: Express Receives It&lt;/h4&gt;
&lt;p&gt;The backend route receives the request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;app.post(&quot;/api/tasks&quot;, async (req, res) =&amp;gt; {
  const { title, description } = req.body;

  // validation and database logic
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-4-backend-validates-the-data&quot;&gt;Step 4: Backend Validates the Data&lt;/h4&gt;
&lt;p&gt;The backend should never blindly trust frontend input.&lt;/p&gt;
&lt;p&gt;It might check:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (!title) {
  return res.status(400).json({
    error: &quot;Title is required&quot;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-5-backend-queries-postgresql&quot;&gt;Step 5: Backend Queries PostgreSQL&lt;/h4&gt;
&lt;p&gt;The server sends an SQL query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO tasks (title, description)
VALUES ($1, $2)
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;$1&lt;/code&gt; and &lt;code&gt;$2&lt;/code&gt; parameters are supplied separately.&lt;/p&gt;
&lt;p&gt;This is important because parameterized queries help protect against SQL injection.&lt;/p&gt;
&lt;h4 id=&quot;step-6-postgresql-processes-the-query&quot;&gt;Step 6: PostgreSQL Processes the Query&lt;/h4&gt;
&lt;p&gt;PostgreSQL validates the query, modifies the appropriate table, and returns the result.&lt;/p&gt;
&lt;h4 id=&quot;step-7-backend-sends-a-response&quot;&gt;Step 7: Backend Sends a Response&lt;/h4&gt;
&lt;p&gt;The backend may return:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: 15,
  &quot;title&quot;: &quot;Learn PostgreSQL&quot;,
  &quot;description&quot;: &quot;Understand relational databases&quot;,
  &quot;completed&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-8-react-updates-the-ui&quot;&gt;Step 8: React Updates the UI&lt;/h4&gt;
&lt;p&gt;React receives the response and updates its state.&lt;/p&gt;
&lt;p&gt;The new task then appears on the screen.&lt;/p&gt;
&lt;p&gt;This is the complete full-stack cycle.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;9-authentication-connecting-users-to-data&quot;&gt;9. Authentication: Connecting Users to Data&lt;/h2&gt;
&lt;p&gt;Most real applications need authentication.&lt;/p&gt;
&lt;p&gt;Users might register with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Name
Email
Password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backend stores account information in PostgreSQL.&lt;/p&gt;
&lt;p&gt;Passwords should &lt;strong&gt;never&lt;/strong&gt; be stored as plain text.&lt;/p&gt;
&lt;p&gt;Instead, applications typically use secure password hashing algorithms such as bcrypt or Argon2.&lt;/p&gt;
&lt;p&gt;After login, the application can issue an authentication token or session.&lt;/p&gt;
&lt;p&gt;A common architecture looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User
 ↓
React Login Form
 ↓
POST /api/login
 ↓
Backend
 ↓
Verify Password
 ↓
PostgreSQL
 ↓
Create Session/Token
 ↓
React
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For protected requests, the frontend sends authentication information with the request.&lt;/p&gt;
&lt;p&gt;The backend verifies it before allowing access to protected resources.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-database-relationships&quot;&gt;10. Database Relationships&lt;/h2&gt;
&lt;p&gt;Real-world applications rarely have only one table.&lt;/p&gt;
&lt;p&gt;For example, a project management application might contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;users
projects
tasks
comments
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A user can have many projects.&lt;/p&gt;
&lt;p&gt;A project can have many tasks.&lt;/p&gt;
&lt;p&gt;A task can have many comments.&lt;/p&gt;
&lt;p&gt;This creates relationships between tables.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(255) UNIQUE NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Projects can reference users:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE projects (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    user_id INTEGER REFERENCES users(id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;user_id&lt;/code&gt; creates a relationship between the project and its owner.&lt;/p&gt;
&lt;p&gt;This is one of the most important concepts when moving from simple applications to production systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;11-join-queries&quot;&gt;11. JOIN Queries&lt;/h2&gt;
&lt;p&gt;Relationships become powerful when combined with SQL joins.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    projects.name,
    users.name AS owner
FROM projects
JOIN users
ON projects.user_id = users.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can return:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Project             Owner
--------------------------------
Marketing Website   John
Mobile Application  Sarah
CRM Dashboard       David
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of storing everything in one giant table, relational databases allow information to be organized into connected structures.&lt;/p&gt;
&lt;p&gt;This improves data consistency and maintainability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;12-validation-must-happen-on-the-backend&quot;&gt;12. Validation Must Happen on the Backend&lt;/h2&gt;
&lt;p&gt;Frontend validation is useful for user experience.&lt;/p&gt;
&lt;p&gt;For example, React can tell a user:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Email is required.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But frontend validation alone is not enough.&lt;/p&gt;
&lt;p&gt;A malicious user can bypass the React interface and send requests directly to the API.&lt;/p&gt;
&lt;p&gt;Therefore, the backend must validate requests independently.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (!email || !email.includes(&quot;@&quot;)) {
  return res.status(400).json({
    error: &quot;Invalid email&quot;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Production applications often use validation libraries to create structured schemas.&lt;/p&gt;
&lt;p&gt;The rule is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Never trust client-side input.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;13-error-handling&quot;&gt;13. Error Handling&lt;/h2&gt;
&lt;p&gt;Full-stack applications must also handle errors gracefully.&lt;/p&gt;
&lt;p&gt;A database connection might fail.&lt;/p&gt;
&lt;p&gt;A user might request a resource that doesn't exist.&lt;/p&gt;
&lt;p&gt;A SQL query might fail.&lt;/p&gt;
&lt;p&gt;A server might experience an unexpected error.&lt;/p&gt;
&lt;p&gt;Instead of exposing internal errors to users, the backend should return appropriate HTTP status codes.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;200 OK
201 Created
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
500 Internal Server Error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React can then display an appropriate message.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (!response.ok) {
  setError(&quot;Unable to load tasks.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Good error handling improves both user experience and debugging.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;14-environment-variables&quot;&gt;14. Environment Variables&lt;/h2&gt;
&lt;p&gt;Database credentials should not be hard-coded into application source code.&lt;/p&gt;
&lt;p&gt;Instead, applications commonly use environment variables.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DATABASE_URL=postgresql://user:password@localhost:5432/myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backend reads the value from the environment.&lt;/p&gt;
&lt;p&gt;This allows different configurations for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Development
Testing
Staging
Production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also helps prevent sensitive credentials from being accidentally committed to Git repositories.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;.env&lt;/code&gt; file should generally be excluded from version control when it contains secrets.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;15-orm-vs-raw-sql&quot;&gt;15. ORM vs Raw SQL&lt;/h2&gt;
&lt;p&gt;Developers can communicate with PostgreSQL using raw SQL or an Object-Relational Mapping tool.&lt;/p&gt;
&lt;p&gt;Raw SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM users WHERE id = $1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ORMs provide a higher-level programming interface.&lt;/p&gt;
&lt;p&gt;Popular choices in the JavaScript ecosystem include tools such as Prisma, Drizzle, and Sequelize.&lt;/p&gt;
&lt;p&gt;An ORM can make common database operations easier to organize.&lt;/p&gt;
&lt;p&gt;However, understanding SQL remains important.&lt;/p&gt;
&lt;p&gt;Using an ORM without understanding databases can create problems when queries become complex or performance issues appear.&lt;/p&gt;
&lt;p&gt;A strong full-stack developer should understand both the abstraction and the underlying database.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;16-database-indexing&quot;&gt;16. Database Indexing&lt;/h2&gt;
&lt;p&gt;As an application grows, database performance becomes increasingly important.&lt;/p&gt;
&lt;p&gt;Suppose a users table contains millions of records.&lt;/p&gt;
&lt;p&gt;A query like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM users
WHERE email = '*Emails are not allowed*';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;could become expensive without appropriate indexing.&lt;/p&gt;
&lt;p&gt;An index can improve lookup performance:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_email
ON users(email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Indexes are powerful, but they also have a cost.&lt;/p&gt;
&lt;p&gt;They consume storage and can increase the cost of insert and update operations.&lt;/p&gt;
&lt;p&gt;Therefore, indexes should be designed around actual query patterns.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;17-transactions&quot;&gt;17. Transactions&lt;/h2&gt;
&lt;p&gt;Some operations require multiple database changes to succeed together.&lt;/p&gt;
&lt;p&gt;Imagine transferring money between two accounts.&lt;/p&gt;
&lt;p&gt;You might need to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Subtract money from Account A
Add money to Account B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the first operation succeeds but the second fails, the database could become inconsistent.&lt;/p&gt;
&lt;p&gt;Transactions solve this problem.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If something goes wrong, the transaction can be rolled back.&lt;/p&gt;
&lt;p&gt;Transactions are an important part of reliable backend systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;18-performance-across-the-full-stack&quot;&gt;18. Performance Across the Full Stack&lt;/h2&gt;
&lt;p&gt;Full-stack performance isn't just about writing fast React components.&lt;/p&gt;
&lt;p&gt;Every layer can introduce delays.&lt;/p&gt;
&lt;p&gt;Consider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React rendering
       ↓
Network request
       ↓
Backend processing
       ↓
Database query
       ↓
Network response
       ↓
React rendering
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the database query takes 2 seconds, optimizing React alone won't solve the problem.&lt;/p&gt;
&lt;p&gt;Similarly, if the API returns a massive JSON response, the frontend may still feel slow.&lt;/p&gt;
&lt;p&gt;Performance should therefore be considered across the entire architecture.&lt;/p&gt;
&lt;p&gt;Possible optimizations include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React code splitting&lt;/li&gt;
&lt;li&gt;Caching&lt;/li&gt;
&lt;li&gt;API pagination&lt;/li&gt;
&lt;li&gt;Database indexing&lt;/li&gt;
&lt;li&gt;Efficient SQL queries&lt;/li&gt;
&lt;li&gt;Connection pooling&lt;/li&gt;
&lt;li&gt;Compression&lt;/li&gt;
&lt;li&gt;CDN usage&lt;/li&gt;
&lt;li&gt;Lazy loading&lt;/li&gt;
&lt;li&gt;Server-side caching&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;19-pagination&quot;&gt;19. Pagination&lt;/h2&gt;
&lt;p&gt;Imagine an application has 100,000 products.&lt;/p&gt;
&lt;p&gt;Returning every product to React at once would be inefficient.&lt;/p&gt;
&lt;p&gt;Instead, the API can use pagination.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET /api/products?page=1&amp;amp;limit=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backend retrieves only the required records.&lt;/p&gt;
&lt;p&gt;SQL might use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM products
ORDER BY id
LIMIT 20 OFFSET 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pagination reduces network traffic and improves application responsiveness.&lt;/p&gt;
&lt;p&gt;For very large datasets, cursor-based pagination can provide better performance than large offsets.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;20-caching&quot;&gt;20. Caching&lt;/h2&gt;
&lt;p&gt;Not every request needs to hit PostgreSQL.&lt;/p&gt;
&lt;p&gt;Suppose thousands of users request the same popular product.&lt;/p&gt;
&lt;p&gt;Repeatedly querying the database can create unnecessary load.&lt;/p&gt;
&lt;p&gt;Caching can store frequently requested information temporarily.&lt;/p&gt;
&lt;p&gt;The architecture might become:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React
 ↓
API
 ↓
Cache
 ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the data exists in the cache, the backend can return it immediately.&lt;/p&gt;
&lt;p&gt;Caching strategies need careful consideration because stale data can create incorrect results.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;21-security-in-a-full-stack-application&quot;&gt;21. Security in a Full-Stack Application&lt;/h2&gt;
&lt;p&gt;Security must exist at every layer.&lt;/p&gt;
&lt;h4 id=&quot;frontend&quot;&gt;Frontend&lt;/h4&gt;
&lt;p&gt;Avoid exposing sensitive information.&lt;/p&gt;
&lt;h4 id=&quot;backend&quot;&gt;Backend&lt;/h4&gt;
&lt;p&gt;Validate input and authenticate users.&lt;/p&gt;
&lt;h4 id=&quot;api&quot;&gt;API&lt;/h4&gt;
&lt;p&gt;Implement authorization and rate limiting where appropriate.&lt;/p&gt;
&lt;h4 id=&quot;database&quot;&gt;Database&lt;/h4&gt;
&lt;p&gt;Use restricted database accounts and secure credentials.&lt;/p&gt;
&lt;h4 id=&quot;sql&quot;&gt;SQL&lt;/h4&gt;
&lt;p&gt;Use parameterized queries.&lt;/p&gt;
&lt;h4 id=&quot;authentication&quot;&gt;Authentication&lt;/h4&gt;
&lt;p&gt;Hash passwords securely and protect sessions or tokens.&lt;/p&gt;
&lt;h4 id=&quot;deployment&quot;&gt;Deployment&lt;/h4&gt;
&lt;p&gt;Use HTTPS and secure environment variables.&lt;/p&gt;
&lt;p&gt;Security isn't one feature that can simply be added at the end.&lt;/p&gt;
&lt;p&gt;It is an architectural responsibility.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;22-development-workflow&quot;&gt;22. Development Workflow&lt;/h2&gt;
&lt;p&gt;A practical development workflow might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Design database schema
2. Build backend API
3. Test API
4. Build React interface
5. Connect frontend to API
6. Add authentication
7. Add validation
8. Add error handling
9. Optimize database queries
10. Test complete application
11. Deploy
12. Monitor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Developers can use Git to track changes throughout the process.&lt;/p&gt;
&lt;p&gt;A typical repository might look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;my-fullstack-app/
│
├── frontend/
│   ├── src/
│   ├── components/
│   └── pages/
│
├── backend/
│   ├── routes/
│   ├── controllers/
│   ├── services/
│   └── middleware/
│
├── database/
│   └── migrations/
│
└── README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact structure varies between projects, but separating responsibilities helps maintainability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;23-testing-the-full-stack&quot;&gt;23. Testing the Full Stack&lt;/h2&gt;
&lt;p&gt;Testing should happen at different levels.&lt;/p&gt;
&lt;h4 id=&quot;frontend-testing&quot;&gt;Frontend Testing&lt;/h4&gt;
&lt;p&gt;Test components and user interactions.&lt;/p&gt;
&lt;h4 id=&quot;api-testing&quot;&gt;API Testing&lt;/h4&gt;
&lt;p&gt;Test endpoints independently.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POST /api/login
GET /api/users
POST /api/tasks
DELETE /api/tasks/:id
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;database-testing&quot;&gt;Database Testing&lt;/h4&gt;
&lt;p&gt;Verify that queries produce the expected results.&lt;/p&gt;
&lt;h4 id=&quot;integration-testing&quot;&gt;Integration Testing&lt;/h4&gt;
&lt;p&gt;Test whether multiple components work together.&lt;/p&gt;
&lt;h4 id=&quot;end-to-end-testing&quot;&gt;End-to-End Testing&lt;/h4&gt;
&lt;p&gt;Simulate the experience of a real user.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Open application
↓
Register
↓
Login
↓
Create task
↓
Edit task
↓
Complete task
↓
Logout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The goal isn't simply to test individual functions.&lt;/p&gt;
&lt;p&gt;It's to verify that the complete system behaves correctly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;24-deployment&quot;&gt;24. Deployment&lt;/h2&gt;
&lt;p&gt;Once the application works locally, it needs to be deployed.&lt;/p&gt;
&lt;p&gt;A typical production architecture might look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User
 ↓
Frontend Hosting / CDN
 ↓
React Application
 ↓
Backend API
 ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend and backend can be deployed separately or together depending on the architecture.&lt;/p&gt;
&lt;p&gt;The production database should have appropriate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backups&lt;/li&gt;
&lt;li&gt;Access controls&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Connection limits&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Deployment is not the end of development.&lt;/p&gt;
&lt;p&gt;Production systems require monitoring and maintenance.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;25-logging-and-monitoring&quot;&gt;25. Logging and Monitoring&lt;/h2&gt;
&lt;p&gt;When something goes wrong in production, developers need information about what happened.&lt;/p&gt;
&lt;p&gt;Backend logging can record events such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request received
User authenticated
Database query executed
Response returned
Error occurred
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Monitoring tools can help identify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slow API responses&lt;/li&gt;
&lt;li&gt;High database usage&lt;/li&gt;
&lt;li&gt;Server errors&lt;/li&gt;
&lt;li&gt;Failed requests&lt;/li&gt;
&lt;li&gt;Memory problems&lt;/li&gt;
&lt;li&gt;Traffic spikes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A production application without observability can be extremely difficult to maintain.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;26-the-most-important-skill-understanding-the-boundaries&quot;&gt;26. The Most Important Skill: Understanding the Boundaries&lt;/h2&gt;
&lt;p&gt;Learning React, Node.js, Express, and PostgreSQL individually is useful.&lt;/p&gt;
&lt;p&gt;But the most important full-stack skill is understanding &lt;strong&gt;where responsibility belongs&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;React should focus primarily on presentation and user interaction.&lt;/p&gt;
&lt;p&gt;The backend should handle business rules, authentication, authorization, and API behavior.&lt;/p&gt;
&lt;p&gt;PostgreSQL should handle persistent data and relational operations.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React:
&quot;What did the user click?&quot;

Backend:
&quot;Is this action allowed?&quot;

Database:
&quot;What information should be stored?&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When responsibilities are clearly separated, applications become easier to understand and maintain.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;27-what-happens-when-you-click-a-button-&quot;&gt;27. What Happens When You Click a Button?&lt;/h2&gt;
&lt;p&gt;Let's summarize the entire process using one simple example.&lt;/p&gt;
&lt;p&gt;A user clicks:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Complete Task&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;React detects the click.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;fetch(&quot;/api/tasks/15&quot;, {
  method: &quot;PATCH&quot;,
  body: JSON.stringify({
    completed: true
  })
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request travels to the backend.&lt;/p&gt;
&lt;p&gt;Express identifies the route.&lt;/p&gt;
&lt;p&gt;The backend verifies the user's authentication.&lt;/p&gt;
&lt;p&gt;The backend validates the input.&lt;/p&gt;
&lt;p&gt;The server sends a parameterized SQL query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;UPDATE tasks
SET completed = $1
WHERE id = $2
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL executes the query.&lt;/p&gt;
&lt;p&gt;The database returns the updated task.&lt;/p&gt;
&lt;p&gt;The backend converts the result into JSON.&lt;/p&gt;
&lt;p&gt;React receives the response.&lt;/p&gt;
&lt;p&gt;React updates its state.&lt;/p&gt;
&lt;p&gt;The interface changes from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;☐ Learn PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;☑ Learn PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The user sees the result almost instantly.&lt;/p&gt;
&lt;p&gt;That tiny interaction represents the entire full stack architecture working together.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;28-what-should-a-beginner-learn-first-&quot;&gt;28. What Should a Beginner Learn First?&lt;/h2&gt;
&lt;p&gt;If you're starting full-stack development, don't try to master everything simultaneously.&lt;/p&gt;
&lt;p&gt;A practical learning path is:&lt;/p&gt;
&lt;h4 id=&quot;step-1-html-and-css&quot;&gt;Step 1: HTML and CSS&lt;/h4&gt;
&lt;p&gt;Understand the structure and styling of websites.&lt;/p&gt;
&lt;h4 id=&quot;step-2-javascript&quot;&gt;Step 2: JavaScript&lt;/h4&gt;
&lt;p&gt;Learn variables, functions, arrays, objects, asynchronous programming, modules, and modern JavaScript.&lt;/p&gt;
&lt;h4 id=&quot;step-3-react&quot;&gt;Step 3: React&lt;/h4&gt;
&lt;p&gt;Learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Components&lt;/li&gt;
&lt;li&gt;Props&lt;/li&gt;
&lt;li&gt;State&lt;/li&gt;
&lt;li&gt;Hooks&lt;/li&gt;
&lt;li&gt;Forms&lt;/li&gt;
&lt;li&gt;Routing&lt;/li&gt;
&lt;li&gt;API requests&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;step-4-node-js-and-express&quot;&gt;Step 4: Node.js and Express&lt;/h4&gt;
&lt;p&gt;Learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP&lt;/li&gt;
&lt;li&gt;REST APIs&lt;/li&gt;
&lt;li&gt;Middleware&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;step-5-postgresql&quot;&gt;Step 5: PostgreSQL&lt;/h4&gt;
&lt;p&gt;Learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Primary keys&lt;/li&gt;
&lt;li&gt;Foreign keys&lt;/li&gt;
&lt;li&gt;CRUD&lt;/li&gt;
&lt;li&gt;JOINs&lt;/li&gt;
&lt;li&gt;Indexes&lt;/li&gt;
&lt;li&gt;Transactions&lt;/li&gt;
&lt;li&gt;Database design&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;step-6-full-stack-projects&quot;&gt;Step 6: Full-Stack Projects&lt;/h4&gt;
&lt;p&gt;Build applications that connect everything together.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Task manager&lt;/li&gt;
&lt;li&gt;Blog platform&lt;/li&gt;
&lt;li&gt;Ecommerce application&lt;/li&gt;
&lt;li&gt;CRM&lt;/li&gt;
&lt;li&gt;Job board&lt;/li&gt;
&lt;li&gt;Inventory system&lt;/li&gt;
&lt;li&gt;Project management platform&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Building projects is where the concepts become real skills.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;29-the-future-of-full-stack-development&quot;&gt;29. The Future of Full-Stack Development&lt;/h2&gt;
&lt;p&gt;Modern full-stack development continues to evolve.&lt;/p&gt;
&lt;p&gt;Developers now have access to powerful frameworks, managed databases, cloud platforms, serverless infrastructure, AI coding assistants, and automated deployment systems.&lt;/p&gt;
&lt;p&gt;However, the fundamental architecture remains remarkably consistent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User
 ↓
Interface
 ↓
API
 ↓
Business Logic
 ↓
Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tools will change.&lt;/p&gt;
&lt;p&gt;Frameworks will change.&lt;/p&gt;
&lt;p&gt;Programming languages will evolve.&lt;/p&gt;
&lt;p&gt;But understanding how data moves through a software system will remain valuable.&lt;/p&gt;
&lt;p&gt;A developer who understands the complete lifecycle of a request can adapt to new technologies much more easily.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Going from React to PostgreSQL is not simply about learning four different technologies.&lt;/p&gt;
&lt;p&gt;It's about understanding how an entire software system works.&lt;/p&gt;
&lt;p&gt;React creates the interface.&lt;/p&gt;
&lt;p&gt;The browser sends requests.&lt;/p&gt;
&lt;p&gt;The backend receives those requests.&lt;/p&gt;
&lt;p&gt;Business logic determines what should happen.&lt;/p&gt;
&lt;p&gt;The API communicates with the database.&lt;/p&gt;
&lt;p&gt;PostgreSQL stores and retrieves information.&lt;/p&gt;
&lt;p&gt;The backend returns a response.&lt;/p&gt;
&lt;p&gt;React updates the interface.&lt;/p&gt;
&lt;p&gt;This cycle happens repeatedly throughout almost every modern data-driven web application.&lt;/p&gt;
&lt;p&gt;The real power of full stack development comes from understanding the connections between these layers.&lt;/p&gt;
&lt;p&gt;You don't need to become an expert in everything immediately. Start with the fundamentals, build small applications, understand how each layer communicates with the next, and gradually introduce more advanced concepts such as authentication, database relationships, caching, indexing, transactions, testing, and deployment.&lt;/p&gt;
&lt;p&gt;Once you understand the journey from a user's click in React to a SQL query in PostgreSQL and back again you stop thinking about technologies as isolated tools.&lt;/p&gt;
&lt;p&gt;You start thinking like a full stack developer.&lt;/p&gt;
&lt;p&gt;And that shift in perspective is one of the most valuable skills you can develop in modern software engineering.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30077/from-react-to-postgresql-understanding-the-full-stack</guid>
<pubDate>Wed, 07 Oct 2026 05:22:39 +0000</pubDate>
</item>
<item>
<title>Why I Chose a Simple Capability Token for Waitless — and When I Would Harden It</title>
<link>https://coderlegion.com/29812/why-i-chose-a-simple-capability-token-for-waitless-and-when-i-would-harden-it</link>
<description>&lt;p&gt;While working on &lt;strong&gt;Waitless&lt;/strong&gt;, I needed customers to be able to track their position in a queue and leave the queue without creating an account.&lt;/p&gt;
&lt;p&gt;That led to a small but interesting security design question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How can I authorize an anonymous user without introducing a full authentication system?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My current solution is deliberately simple: each queue entry receives a random UUID token, and that token is used as a capability for the tracking link.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;uuid-token-instead-of-a-sequential-id&quot;&gt;UUID token instead of a sequential ID&lt;/h3&gt;
&lt;p&gt;A sequential identifier could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/track/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is that IDs such as &lt;code&gt;123&lt;/code&gt;, &lt;code&gt;124&lt;/code&gt;, and &lt;code&gt;125&lt;/code&gt; are predictable and enumerable.&lt;/p&gt;
&lt;p&gt;For an anonymous tracking system, I don't want a user to be able to guess another customer's resource simply by changing the ID.&lt;/p&gt;
&lt;p&gt;Instead, an entry has a separate token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-prisma&quot;&gt;model Entry {
  id      String @id @default(uuid())
  token   String @unique @default(uuid())

  name     String
  email    String?
  position Int
  status   EntryStatus @default(WAITING)

  queueId String
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The public URL therefore looks more like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/track/550e8400-e29b-41d4-a716-446655440000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important point is that the token is not intended to be an ordinary identifier.&lt;/p&gt;
&lt;p&gt;It acts as a &lt;strong&gt;capability&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-a-capability-url-&quot;&gt;What is a capability URL?&lt;/h3&gt;
&lt;p&gt;A capability can be thought of as:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;An unguessable secret that grants access to a specific resource.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In my case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;possess valid token
        ↓
access this queue entry
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The capability is deliberately designed to be difficult to guess.&lt;/p&gt;
&lt;p&gt;That is different from simply hiding a sequential ID and hoping nobody discovers it.&lt;/p&gt;
&lt;p&gt;This distinction matters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Security by obscurity
→ &quot;Nobody knows the ID.&quot;

Capability-based design
→ &quot;Access requires possession of a deliberately unguessable credential.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second is a much more intentional security model.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-i-kept-the-token-simple-for-now&quot;&gt;Why I kept the token simple for now&lt;/h3&gt;
&lt;p&gt;Waitless is an anonymous queueing system.&lt;/p&gt;
&lt;p&gt;A customer does not need an account, password, or traditional login just to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;see their position&lt;/li&gt;
&lt;li&gt;receive queue updates&lt;/li&gt;
&lt;li&gt;leave the queue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Introducing a complete authentication system would add considerable complexity to the product and to the user experience.&lt;/p&gt;
&lt;p&gt;For the current use case, a capability link gives me a simple model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Join queue
    ↓
Generate random token
    ↓
Give customer tracking link
    ↓
Customer presents token
    ↓
Server identifies the queue entry
    ↓
Allow limited actions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a conscious trade-off.&lt;/p&gt;
&lt;p&gt;I'm choosing &lt;strong&gt;simplicity and frictionless anonymous access&lt;/strong&gt; because the current resource is relatively limited in scope.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;but-the-token-is-also-a-bearer-credential&quot;&gt;But the token is also a bearer credential&lt;/h3&gt;
&lt;p&gt;There is an important consequence.&lt;/p&gt;
&lt;p&gt;If possession of the token grants access, then:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;whoever possesses the token can use it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That makes the token a bearer credential.&lt;/p&gt;
&lt;p&gt;For example, the current &lt;code&gt;leave()&lt;/code&gt; operation relies on the same tracking capability.&lt;/p&gt;
&lt;p&gt;So if the token is leaked, copied, logged, or shared, another person could potentially use it.&lt;/p&gt;
&lt;p&gt;That means the design is intentionally simple, but it is not the strongest possible security model.&lt;/p&gt;
&lt;h4 id=&quot;known-limitation&quot;&gt;Known limitation&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;The tracking token currently also functions as a bearer credential for &lt;code&gt;leave()&lt;/code&gt;, with no rate-limiting or one-time consumption.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I consider this an accepted limitation of the current design rather than something I want to hide.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-stronger-design-capability-url-session-cookie&quot;&gt;A stronger design: capability URL → session cookie&lt;/h3&gt;
&lt;p&gt;One way to harden the system would be to use the tracking link only as an initial capability.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/claim/&amp;lt;secret&amp;gt;
      ↓
validate capability
      ↓
create anonymous session
      ↓
HttpOnly + Secure cookie
      ↓
redirect to /track
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, the token would no longer need to remain in the URL.&lt;/p&gt;
&lt;p&gt;The browser would use the session cookie for future requests.&lt;/p&gt;
&lt;p&gt;This would reduce some of the risks associated with repeatedly carrying a secret in a URL and would give me more control over session lifecycle, expiration and revocation.&lt;/p&gt;
&lt;p&gt;But it introduces another trade-off.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-trade-off-security-vs-convenience&quot;&gt;The trade-off: security vs. convenience&lt;/h3&gt;
&lt;p&gt;The current capability link has an important usability property:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The link itself works across devices.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A customer can open it on their phone, copy it to another device, bookmark it, or reopen it later.&lt;/p&gt;
&lt;p&gt;With a one-time capability-to-session exchange, that behavior becomes more complicated.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Phone
  ↓
claim token
  ↓
session created
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the same one-time token may no longer be usable on another device.&lt;/p&gt;
&lt;p&gt;Supporting multiple devices would therefore require additional identity/session handling, which increases complexity.&lt;/p&gt;
&lt;p&gt;For a simple anonymous queue, I decided that this trade-off is not currently justified.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-would-this-design-change-&quot;&gt;When would this design change?&lt;/h2&gt;
&lt;p&gt;The answer is: &lt;strong&gt;when the product starts having identity requirements.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Imagine Waitless later introduces a reservation system.&lt;/p&gt;
&lt;p&gt;Now a customer might have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Account
   ↓
Reservations
   ↓
Queue entries
   ↓
Payment history
   ↓
Cancellation rules
   ↓
Personal information
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The security requirements become much stronger.&lt;/p&gt;
&lt;p&gt;At that point, a random tracking capability may no longer be enough as the main authorization mechanism.&lt;/p&gt;
&lt;p&gt;I would probably move toward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User authentication
        ↓
Authenticated session
        ↓
Authorization
        ↓
Reservation / queue access
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The capability token could still exist for specific use cases, but it would no longer be responsible for the entire identity model.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;uuidv4-vs-uuidv7&quot;&gt;UUIDv4 vs. UUIDv7&lt;/h2&gt;
&lt;p&gt;This also made me think more carefully about what an identifier is supposed to do.&lt;/p&gt;
&lt;p&gt;For database identifiers, UUIDv7 is attractive because it is time-ordered and therefore provides better index locality than completely random UUIDv4 values.&lt;/p&gt;
&lt;p&gt;For a public capability, however, my primary requirement is different:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;I care more about unpredictability than database ordering.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's why I would keep the concepts separate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Database ID
    ↓
UUIDv7 can be a good choice

Public capability token
    ↓
Cryptographically random token / UUIDv4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an example of a broader engineering principle:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The best identifier for the database is not necessarily the best credential for the client.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-about-storing-the-token-itself-&quot;&gt;What about storing the token itself?&lt;/h2&gt;
&lt;p&gt;Another improvement would be to store only a hash of the capability:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;raw token
    ↓
SHA-256
    ↓
tokenHash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the database contains the verifier rather than the original bearer secret.&lt;/p&gt;
&lt;p&gt;The application can hash the presented token and compare the result.&lt;/p&gt;
&lt;p&gt;This gives additional protection if the database contents are exposed.&lt;/p&gt;
&lt;p&gt;Again, it adds some implementation complexity, so it is a hardening step rather than something I needed to introduce simply to make the current Waitless flow work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-current-decision&quot;&gt;My current decision&lt;/h2&gt;
&lt;p&gt;For the current version of Waitless:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Random capability token
        ↓
Tracking URL
        ↓
Anonymous access
        ↓
Limited actions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I deliberately chose this because it provides a very small and understandable authorization mechanism without forcing customers through registration and login.&lt;/p&gt;
&lt;p&gt;The trade-off is that the token is a bearer credential, so the system needs to treat it as sensitive.&lt;/p&gt;
&lt;p&gt;I have therefore documented the limitation rather than pretending the design is perfect.&lt;/p&gt;
&lt;p&gt;My planned evolution is roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Current
Random capability URL
        ↓
Simple anonymous access


Later
Rate limiting
+ expiry / revocation
+ stronger token handling


When identity becomes important
        ↓
Capability URL
        ↓
Session / authentication
        ↓
Fine-grained authorization
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interesting lesson for me was not simply “use UUIDs.”&lt;/p&gt;
&lt;p&gt;It was learning to ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What is this value actually supposed to represent?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;An identifier, a capability, and an authenticated identity are three different concepts.&lt;/p&gt;
&lt;p&gt;Once that distinction is clear, the architecture becomes much easier to reason about.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29812/why-i-chose-a-simple-capability-token-for-waitless-and-when-i-would-harden-it</guid>
<pubDate>Mon, 05 Oct 2026 07:20:50 +0000</pubDate>
</item>
<item>
<title>Why I Used BullMQ + Redis for Background Jobs in My NestJS Application — and When I Wouldn't</title>
<link>https://coderlegion.com/29737/why-i-used-bullmq-redis-for-background-jobs-in-my-nestjs-application-and-when-i-wouldnt</link>
<description>&lt;p&gt;While working on &lt;strong&gt;Waitless&lt;/strong&gt;, a real-time queue management platform, I came across a backend problem that looked simple at first:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A customer reaches the front of the queue, and the system needs to send notifications to other customers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first instinct could be to simply send the email inside the HTTP request.&lt;/p&gt;
&lt;p&gt;But once I looked at the problem as a backend architecture problem rather than just an implementation task, I realized that sending the email and handling the user's request are two different workloads.&lt;/p&gt;
&lt;p&gt;That led me to &lt;strong&gt;BullMQ + Redis&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The interesting part wasn't learning the API. The interesting part was understanding &lt;strong&gt;why this architecture made sense, what Redis was actually doing, and when I would choose something else instead.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-in-waitless&quot;&gt;The problem in Waitless&lt;/h3&gt;
&lt;p&gt;Waitless allows customers to join a digital queue and track their position without creating an account.&lt;/p&gt;
&lt;p&gt;When one customer is called, another customer may need to receive a notification such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“It's your turn.”&lt;/li&gt;
&lt;li&gt;“You're almost next.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I already had the queue state stored in PostgreSQL. The question was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Should the API send the email immediately, or should it create background work?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A synchronous approach could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTTP request
     ↓
NestJS
     ↓
PostgreSQL
     ↓
Send email
     ↓
HTTP response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is that sending an email depends on an external service.&lt;/p&gt;
&lt;p&gt;If that service is slow or temporarily unavailable, my API request is affected.&lt;/p&gt;
&lt;p&gt;I don't necessarily need the email to be completed before responding to the user.&lt;/p&gt;
&lt;p&gt;What I actually need is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Remember that this email has to be sent, and process it separately.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is exactly the kind of problem a job queue is designed to solve.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;introducing-bullmq&quot;&gt;Introducing BullMQ&lt;/h2&gt;
&lt;p&gt;BullMQ provides a job queue abstraction for Node.js applications.&lt;/p&gt;
&lt;p&gt;Instead of doing the work immediately, the application creates a job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Application
     ↓
   Queue
     ↓
    Job
     ↓
   Worker
     ↓
 Actual work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application that creates the job is the &lt;strong&gt;producer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The worker that processes the job is the &lt;strong&gt;consumer/worker&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This gives me a clean separation between:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Creating work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Executing work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That separation becomes particularly useful when the work is slow, retryable, delayed, or needs to be processed independently from the HTTP request.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-redis-comes-in&quot;&gt;Where Redis comes in&lt;/h2&gt;
&lt;p&gt;One thing I initially had to clarify for myself was the relationship between BullMQ and Redis.&lt;/p&gt;
&lt;p&gt;They are not the same thing.&lt;/p&gt;
&lt;p&gt;A simplified mental model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NestJS
   ↓
BullMQ
   ↓
Redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BullMQ is the queue system.&lt;/p&gt;
&lt;p&gt;Redis is the backend that BullMQ uses to store and coordinate queue-related state.&lt;/p&gt;
&lt;p&gt;In my NestJS application, I configure the connection like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;BullModule.forRoot({
  connection: {
    host: 'localhost',
    port: 6379,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Port &lt;code&gt;6379&lt;/code&gt; is Redis's default port, so this configuration tells BullMQ which Redis instance to connect to.&lt;/p&gt;
&lt;p&gt;The important distinction is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;BullMQ is not “running on port 6379.”&lt;br&gt;
BullMQ is connecting to the Redis server running there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;creating-a-notification-job&quot;&gt;Creating a notification job&lt;/h2&gt;
&lt;p&gt;In Waitless, I inject the notification queue into my service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@InjectQueue('notifications')
private notifQueue: Queue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I can add jobs to that queue.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (next.email) {
  await this.notifQueue.add('your-turn-now', {
    email: next.email,
    name: next.name,
    queueName: queue.name,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This does not mean:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Send the email now.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It means:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Create a job called &lt;code&gt;your-turn-now&lt;/code&gt; and put it into the notifications queue.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The payload contains the information the worker will need later.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NestJS
   ↓
notifQueue.add(...)
   ↓
BullMQ
   ↓
Redis
   ↓
notifications queue
   ↓
your-turn-now job
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual email is the responsibility of the worker.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;delayed-jobs&quot;&gt;Delayed jobs&lt;/h2&gt;
&lt;p&gt;One of the features I needed was delayed notification.&lt;/p&gt;
&lt;p&gt;If another customer is second in line, I want to notify them shortly before their turn.&lt;/p&gt;
&lt;p&gt;My implementation looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const secondInLine =
  await this.entryRepo.findSecondWaiting(queueId, next.id);

if (secondInLine?.email) {
  await this.notifQueue.add(
    'your-turn-soon',
    {
      email: secondInLine.email,
      name: secondInLine.name,
      position: 2,
      queueName: queue.name,
      trackingUrl: `http://localhost:3000/track/${secondInLine.token}`,
    },
    {
      delay: 30_000,
    },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;{ delay: 30_000 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The job doesn't need to be processed immediately.&lt;/p&gt;
&lt;p&gt;The architecture becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Customer becomes second in line
            ↓
      Create BullMQ job
            ↓
       Delayed job
            ↓
          30 sec
            ↓
        Worker picks it up
            ↓
        Send notification
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much more reliable than simply doing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;setTimeout(() =&amp;gt; {
  sendEmail();
}, 30_000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A timer belongs to the application process itself. A queue gives the work an explicit lifecycle outside the original request.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-not-just-use-postgresql-for-the-queue-&quot;&gt;Why not just use PostgreSQL for the queue?&lt;/h2&gt;
&lt;p&gt;This was one of the more interesting questions I explored.&lt;/p&gt;
&lt;p&gt;At first glance, PostgreSQL looks like a perfectly reasonable choice.&lt;/p&gt;
&lt;p&gt;I could create a table such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;jobs
-------------------
id
type
payload
status
created_at
processed_at
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and continuously insert and delete jobs.&lt;/p&gt;
&lt;p&gt;The problem is that the queue workload has different characteristics from my core business data.&lt;/p&gt;
&lt;p&gt;My PostgreSQL database is responsible for things such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;businesses
queues
customers
queue entries
relationships
transactions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those are my core relational business data.&lt;/p&gt;
&lt;p&gt;The queue workload is different:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;job created
job becomes active
job succeeds
job fails
job retries
job is removed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a &lt;strong&gt;high-churn operational workload&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With PostgreSQL's MVCC model, repeatedly inserting and deleting rows creates dead tuples that later have to be cleaned up by VACUUM.&lt;/p&gt;
&lt;p&gt;That doesn't mean PostgreSQL cannot implement queues. It absolutely can.&lt;/p&gt;
&lt;p&gt;The real architectural question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Do I want the queue workload competing with my transactional business workload inside the same database?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For a small application, the answer might be yes.&lt;/p&gt;
&lt;p&gt;For a higher-throughput system, separating those workloads can become valuable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-redis-made-sense-for-my-queue&quot;&gt;Why Redis made sense for my queue&lt;/h2&gt;
&lt;p&gt;Redis is designed around very fast operations on in-memory data structures.&lt;/p&gt;
&lt;p&gt;That makes it a natural fit for workloads such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;caching&lt;/li&gt;
&lt;li&gt;counters&lt;/li&gt;
&lt;li&gt;temporary state&lt;/li&gt;
&lt;li&gt;queues&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;pub/sub&lt;/li&gt;
&lt;li&gt;fast lookups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my notification queue, the important characteristics were:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Asynchronous
High frequency
Short-lived job state
Low latency
Retries
Delayed execution
Worker concurrency
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PostgreSQL
   ├── business data
   └── queue workload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could separate them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PostgreSQL
     ↓
Business state


Redis
     ↓
BullMQ
     ↓
Background jobs
     ↓
Workers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That separation was more important to me than simply saying:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Redis is faster.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-learned-about-workload-driven-architecture&quot;&gt;What I learned about workload-driven architecture&lt;/h2&gt;
&lt;p&gt;This changed the way I think about databases.&lt;/p&gt;
&lt;p&gt;The question shouldn't simply be:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which database is better?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A better question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What workload am I dealing with?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Requirement                    &lt;/th&gt;&lt;th&gt;Possible choice&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Relational transactional data  &lt;/td&gt;&lt;td&gt;PostgreSQL&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Very fast temporary state      &lt;/td&gt;&lt;td&gt;Redis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Background jobs with retries   &lt;/td&gt;&lt;td&gt;BullMQ + Redis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Document-oriented event data   &lt;/td&gt;&lt;td&gt;MongoDB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Large event streams and replay &lt;/td&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Small simple background jobs   &lt;/td&gt;&lt;td&gt;PostgreSQL or a scheduler&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Managed cloud queue            &lt;/td&gt;&lt;td&gt;SQS or similar service&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These aren't absolute rules.&lt;/p&gt;
&lt;p&gt;They are starting points for architectural reasoning.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;bullmq-gives-me-more-than-a-queue&quot;&gt;BullMQ gives me more than a queue&lt;/h2&gt;
&lt;p&gt;Another thing I learned is that the real value of a job system is not just putting something into a list.&lt;/p&gt;
&lt;p&gt;BullMQ provides concepts such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Jobs
Workers
Retries
Backoff
Delayed jobs
Concurrency
Rate limiting
Failed jobs
Job lifecycle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That matters because real systems fail.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Worker
   ↓
Send email
   ↓
Email provider unavailable
   ↓
Job fails
   ↓
Retry
   ↓
Worker tries again
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The system can therefore treat background work as something with a lifecycle instead of a simple function call.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;but-there-is-an-important-failure-model&quot;&gt;But there is an important failure model&lt;/h2&gt;
&lt;p&gt;A queue doesn't automatically mean:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“The job runs exactly once.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A worker can crash.&lt;/p&gt;
&lt;p&gt;A network call can time out.&lt;/p&gt;
&lt;p&gt;A job can be retried.&lt;/p&gt;
&lt;p&gt;The same operation may therefore be executed more than once.&lt;/p&gt;
&lt;p&gt;That means job handlers should often be designed with &lt;strong&gt;idempotency&lt;/strong&gt; in mind.&lt;/p&gt;
&lt;p&gt;For example, a payment operation is much more dangerous to retry blindly than sending a harmless notification.&lt;/p&gt;
&lt;p&gt;This was an important lesson for me:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Using a queue changes the failure model of the application.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You have to think about retries, duplicates, timeouts, and partial failures.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;could-i-use-bullmq-without-redis-&quot;&gt;Could I use BullMQ without Redis?&lt;/h2&gt;
&lt;p&gt;Interestingly, yes.&lt;/p&gt;
&lt;p&gt;Modern BullMQ also provides a PostgreSQL backend.&lt;/p&gt;
&lt;p&gt;That means the conceptual architecture can also be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NestJS
   ↓
BullMQ
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can make sense when I want to keep the infrastructure simpler and the queue workload is moderate.&lt;/p&gt;
&lt;p&gt;But there is a trade-off.&lt;/p&gt;
&lt;p&gt;Using PostgreSQL for both workloads means:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PostgreSQL
 ├── transactional business data
 └── queue workload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the queue and business operations share database resources.&lt;/p&gt;
&lt;p&gt;That can be perfectly reasonable for a smaller application.&lt;/p&gt;
&lt;p&gt;For a system with large queue throughput, isolating the queue workload may become more attractive.&lt;/p&gt;
&lt;p&gt;This is why there isn't one universally correct answer.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-i-would-not-use-bullmq-redis&quot;&gt;When I would NOT use BullMQ + Redis&lt;/h2&gt;
&lt;p&gt;This was probably the most useful part of the exercise.&lt;/p&gt;
&lt;p&gt;I don't want to add Redis and BullMQ just because they are popular technologies.&lt;/p&gt;
&lt;p&gt;For something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const result = calculateTax(data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;there is no reason to create a queue.&lt;/p&gt;
&lt;p&gt;If a task takes a few milliseconds and the user needs the result immediately, synchronous processing is simpler.&lt;/p&gt;
&lt;p&gt;I also might avoid BullMQ for a very small scheduled task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Every day at 08:00
Generate one report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A simple scheduler could be enough.&lt;/p&gt;
&lt;p&gt;And if the application already uses PostgreSQL and has only a small number of background jobs, a PostgreSQL-backed solution may be simpler than adding another infrastructure component.&lt;/p&gt;
&lt;p&gt;The lesson is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Don't introduce distributed infrastructure without a workload that justifies it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-decision-i-now-make-before-choosing-a-queue&quot;&gt;The decision I now make before choosing a queue&lt;/h2&gt;
&lt;p&gt;Instead of thinking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Should I use BullMQ?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ask:&lt;/p&gt;
&lt;h4 id=&quot;1-does-this-work-need-to-happen-asynchronously-&quot;&gt;1. Does this work need to happen asynchronously?&lt;/h4&gt;
&lt;p&gt;If yes, continue.&lt;/p&gt;
&lt;h4 id=&quot;2-does-it-need-retries-or-failure-recovery-&quot;&gt;2. Does it need retries or failure recovery?&lt;/h4&gt;
&lt;p&gt;If yes, a real job system becomes more useful.&lt;/p&gt;
&lt;h4 id=&quot;3-does-it-need-delayed-execution-&quot;&gt;3. Does it need delayed execution?&lt;/h4&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Do this after 30 seconds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;4-does-it-need-multiple-workers-or-concurrency-&quot;&gt;4. Does it need multiple workers or concurrency?&lt;/h4&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Queue
 ├── Worker 1
 ├── Worker 2
 └── Worker 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-does-the-workload-justify-another-infrastructure-component-&quot;&gt;5. Does the workload justify another infrastructure component?&lt;/h4&gt;
&lt;p&gt;This is the trade-off question.&lt;/p&gt;
&lt;p&gt;Only after answering those questions would I choose the technology.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-current-mental-model&quot;&gt;My current mental model&lt;/h2&gt;
&lt;p&gt;The biggest thing I learned from implementing this feature is that architecture is less about memorizing technologies and more about matching &lt;strong&gt;workloads to the right tools&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For Waitless, my current architecture can be thought of as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    Waitless
                       │
          ┌────────────┼────────────┐
          ↓            ↓            ↓
     PostgreSQL      Redis       MongoDB
          │            │            │
   business state    jobs      event history
                    BullMQ
                       │
                    Workers
                       │
                   Email/API
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What is the current business state?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Redis + BullMQ answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What work needs to be processed asynchronously?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MongoDB answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What happened in the event history, and what can I derive from those events?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The important thing is not that I used three technologies.&lt;/p&gt;
&lt;p&gt;The important thing is that each technology was chosen for a different workload.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-takeaway&quot;&gt;Final takeaway&lt;/h2&gt;
&lt;p&gt;The best lesson I took from this wasn't actually “how to use BullMQ.”&lt;/p&gt;
&lt;p&gt;It was this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Before choosing a technology, understand the workload.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What problem am I solving?
What are the access patterns?
What guarantees do I need?
How does the system fail?
What are the trade-offs?
What simpler alternative could work?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I started thinking this way, Redis, BullMQ, PostgreSQL, MongoDB and other technologies stopped looking like isolated tools.&lt;/p&gt;
&lt;p&gt;They became different tools for different engineering problems.&lt;/p&gt;
&lt;p&gt;And that is the mindset I want to continue developing as I work toward designing larger backend systems.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29737/why-i-used-bullmq-redis-for-background-jobs-in-my-nestjs-application-and-when-i-wouldnt</guid>
<pubDate>Sun, 04 Oct 2026 15:45:42 +0000</pubDate>
</item>
<item>
<title>Next.js PostgreSQL: Build a Complete Full Stack Application</title>
<link>https://coderlegion.com/29558/next-js-postgresql-build-a-complete-full-stack-application</link>
<description>&lt;p&gt;Building a modern full stack application is about much more than creating attractive user interfaces. A successful application needs a reliable frontend, a powerful backend, secure authentication, efficient database management, and a smooth user experience.&lt;/p&gt;
&lt;p&gt;Among the many technologies available today, Next.js and PostgreSQL provide a powerful combination for building scalable, maintainable, and production-ready web applications.&lt;/p&gt;
&lt;p&gt;Next.js offers a modern React-based framework with server-side rendering, routing, server components, and backend capabilities. PostgreSQL provides a reliable relational database for storing, managing, and retrieving structured information.&lt;/p&gt;
&lt;p&gt;When these technologies work together, developers can create applications such as task management platforms, project management dashboards, ecommerce systems, customer relationship management tools, and SaaS products.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to build a complete full stack application using Next.js and PostgreSQL, covering architecture, database design, authentication, CRUD operations, API development, security, testing, and deployment.&lt;/p&gt;
&lt;p&gt;The goal is not simply to make an application work. It is to understand how different parts of a real-world software system communicate with one another.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-the-technology-stack&quot;&gt;1. Understanding the Technology Stack&lt;/h3&gt;
&lt;p&gt;Before starting development, it is important to understand the responsibilities of each technology.&lt;/p&gt;
&lt;h4 id=&quot;what-is-next-js-&quot;&gt;What Is Next.js?&lt;/h4&gt;
&lt;p&gt;Next.js is a React framework designed for building modern web applications. It provides several features that simplify frontend and backend development.&lt;/p&gt;
&lt;p&gt;Important features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File-based routing&lt;/li&gt;
&lt;li&gt;Server Components&lt;/li&gt;
&lt;li&gt;Client Components&lt;/li&gt;
&lt;li&gt;Server-side rendering&lt;/li&gt;
&lt;li&gt;Static site generation&lt;/li&gt;
&lt;li&gt;API route handlers&lt;/li&gt;
&lt;li&gt;Server Actions&lt;/li&gt;
&lt;li&gt;Image optimization&lt;/li&gt;
&lt;li&gt;Built-in performance improvements&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of the biggest advantages of Next.js is that developers can build frontend interfaces and server-side functionality within a single project.&lt;/p&gt;
&lt;p&gt;Instead of maintaining completely separate frontend and backend repositories for every application, developers can organize much of the application logic in one codebase.&lt;/p&gt;
&lt;h4 id=&quot;what-is-postgresql&quot;&gt;What Is PostgreSQL&lt;/h4&gt;
&lt;p&gt;PostgreSQL is an open-source relational database management system known for reliability, data integrity, and advanced querying capabilities.&lt;/p&gt;
&lt;p&gt;It supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Relational tables&lt;/li&gt;
&lt;li&gt;Primary and foreign keys&lt;/li&gt;
&lt;li&gt;Transactions&lt;/li&gt;
&lt;li&gt;Indexing&lt;/li&gt;
&lt;li&gt;Complex SQL queries&lt;/li&gt;
&lt;li&gt;JSON and JSONB data&lt;/li&gt;
&lt;li&gt;Constraints&lt;/li&gt;
&lt;li&gt;Database relationships&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For applications involving users, products, orders, tasks, payments, or business records, PostgreSQL offers a structured way to manage information.&lt;/p&gt;
&lt;h4 id=&quot;why-combine-next-js-and-postgresql&quot;&gt;Why Combine Next.js and PostgreSQL&lt;/h4&gt;
&lt;p&gt;The combination provides a clear separation of responsibilities.&lt;/p&gt;
&lt;p&gt;Next.js handles the user interface, application routing, and server-side logic, while PostgreSQL manages persistent application data.&lt;/p&gt;
&lt;p&gt;A typical architecture looks like this:&lt;/p&gt;
&lt;p&gt;User → Next.js Frontend → Server Logic → PostgreSQL Database&lt;/p&gt;
&lt;p&gt;When a user creates a task, for example, the frontend sends the information to the server. The server validates the request, checks authorization, and executes a database operation. PostgreSQL stores the task, and the application returns the updated information to the user.&lt;/p&gt;
&lt;p&gt;This architecture is suitable for applications that need reliable data handling and room for future growth.&lt;/p&gt;
&lt;h3 id=&quot;2-planning-the-application-before-coding&quot;&gt;2. Planning the Application Before Coding&lt;/h3&gt;
&lt;p&gt;Let's build a practical project: a Task Management Application.&lt;/p&gt;
&lt;p&gt;Users will be able to register, log in, create tasks, update task details, mark tasks as completed, and delete records.&lt;/p&gt;
&lt;h4 id=&quot;core-features&quot;&gt;Core Features&lt;/h4&gt;
&lt;p&gt;Our application will include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User registration and login&lt;/li&gt;
&lt;li&gt;Secure authentication&lt;/li&gt;
&lt;li&gt;Personal task dashboard&lt;/li&gt;
&lt;li&gt;Create, read, update, and delete operations&lt;/li&gt;
&lt;li&gt;Task status management&lt;/li&gt;
&lt;li&gt;PostgreSQL database integration&lt;/li&gt;
&lt;li&gt;Form validation&lt;/li&gt;
&lt;li&gt;Responsive design&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Production deployment&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Planning these features before writing code helps prevent unnecessary complexity.&lt;/p&gt;
&lt;p&gt;It also allows us to identify the database tables, relationships, routes, and security requirements early.&lt;/p&gt;
&lt;h3 id=&quot;3-setting-up-the-next-js-project&quot;&gt;3. Setting Up the Next.js Project&lt;/h3&gt;
&lt;p&gt;First, ensure that Node.js and npm are installed.&lt;/p&gt;
&lt;p&gt;Create a new Next.js application using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-next-app@latest next-postgres-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During setup, choose TypeScript, ESLint, Tailwind CSS, and the App Router.&lt;/p&gt;
&lt;p&gt;Move into the project directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd next-postgres-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the development server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open the application in your browser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now see the default Next.js application.&lt;/p&gt;
&lt;h4 id=&quot;recommended-project-structure&quot;&gt;Recommended Project Structure&lt;/h4&gt;
&lt;p&gt;A clean folder structure makes a full stack project easier to maintain.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;next-postgres-app/
│
├── app/
│   ├── dashboard/
│   │   └── page.tsx
│   ├── login/
│   │   └── page.tsx
│   ├── register/
│   │   └── page.tsx
│   ├── api/
│   │   └── tasks/
│   │       └── route.ts
│   ├── layout.tsx
│   └── page.tsx
│
├── components/
│   ├── Navbar.tsx
│   ├── TaskForm.tsx
│   └── TaskList.tsx
│
├── lib/
│   ├── db.ts
│   └── auth.ts
│
├── prisma/
│   └── schema.prisma
│
├── public/
├── .env
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This structure separates pages, reusable components, database utilities, and application logic.&lt;/p&gt;
&lt;h3 id=&quot;4-connecting-postgresql-to-next-js&quot;&gt;4. Connecting PostgreSQL to Next.js&lt;/h3&gt;
&lt;p&gt;There are several ways to connect PostgreSQL to a Next.js application.&lt;/p&gt;
&lt;p&gt;Developers can use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prisma ORM&lt;/li&gt;
&lt;li&gt;Drizzle ORM&lt;/li&gt;
&lt;li&gt;node-postgres (pg)&lt;/li&gt;
&lt;li&gt;Raw SQL queries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For this tutorial, we will use Prisma because it provides schema management, type-safe database queries, and migration support.&lt;/p&gt;
&lt;p&gt;Install Prisma:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install prisma @prisma/client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initialize Prisma:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a Prisma configuration and schema file.&lt;/p&gt;
&lt;h4 id=&quot;configure-the-database-connection&quot;&gt;Configure the Database Connection&lt;/h4&gt;
&lt;p&gt;Inside the &lt;code&gt;.env&lt;/code&gt; file, define your PostgreSQL connection string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;DATABASE_URL=&quot;postgresql://postgres:password@localhost:5432/taskdb&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the username, password, host, and database name with your actual PostgreSQL configuration.&lt;/p&gt;
&lt;p&gt;Never commit real database credentials to a public GitHub repository.&lt;/p&gt;
&lt;p&gt;For production, use environment variables supplied by your hosting provider.&lt;/p&gt;
&lt;h3 id=&quot;5-designing-the-postgresql-database&quot;&gt;5. Designing the PostgreSQL Database&lt;/h3&gt;
&lt;p&gt;Database design is one of the most important parts of full stack development.&lt;/p&gt;
&lt;p&gt;A poorly structured database can create duplicate records, inconsistent information, and difficult queries.&lt;/p&gt;
&lt;p&gt;For our application, we need two primary entities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User&lt;/li&gt;
&lt;li&gt;Task&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each user can have multiple tasks, but each task belongs to one user.&lt;/p&gt;
&lt;p&gt;This is a one-to-many relationship.&lt;/p&gt;
&lt;h4 id=&quot;prisma-database-schema&quot;&gt;Prisma Database Schema&lt;/h4&gt;
&lt;p&gt;Open &lt;code&gt;prisma/schema.prisma&lt;/code&gt; and define the models:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-prisma&quot;&gt;model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  password  String
  tasks     Task[]
  createdAt DateTime @default(now())
}

model Task {
  id          Int      @id @default(autoincrement())
  title       String
  description String?
  completed   Boolean  @default(false)
  userId      Int
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt   DateTime @default(now())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;User&lt;/code&gt; model stores account information, while the &lt;code&gt;Task&lt;/code&gt; model stores task details.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;userId&lt;/code&gt; field connects each task to its owner.&lt;/p&gt;
&lt;p&gt;The unique constraint on email prevents multiple accounts from using the same email address.&lt;/p&gt;
&lt;p&gt;The cascade delete behavior ensures that associated tasks are removed when their user record is deleted. In an actual production system, account deletion policies should be designed carefully.&lt;/p&gt;
&lt;h4 id=&quot;run-the-database-migration&quot;&gt;Run the Database Migration&lt;/h4&gt;
&lt;p&gt;Execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma migrate dev --name init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generate the Prisma Client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma generate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your database tables are now ready.&lt;/p&gt;
&lt;h3 id=&quot;6-creating-a-reusable-database-client&quot;&gt;6. Creating a Reusable Database Client&lt;/h3&gt;
&lt;p&gt;Instead of creating a new database connection in every file, we can create a reusable Prisma client.&lt;/p&gt;
&lt;p&gt;Inside &lt;code&gt;lib/db.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { PrismaClient } from &quot;@prisma/client&quot;;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== &quot;production&quot;) {
  globalForPrisma.prisma = prisma;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern helps prevent unnecessary Prisma client instances during development hot reloads.&lt;/p&gt;
&lt;p&gt;It is especially useful when building applications with multiple server-side modules.&lt;/p&gt;
&lt;h3 id=&quot;7-building-the-first-user-interface&quot;&gt;7. Building the First User Interface&lt;/h3&gt;
&lt;p&gt;Now let's create a simple homepage.&lt;/p&gt;
&lt;p&gt;Inside &lt;code&gt;app/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function Home() {
  return (
    &amp;lt;main className=&quot;min-h-screen flex flex-col items-center justify-center&quot;&amp;gt;
      &amp;lt;h1 className=&quot;text-4xl font-bold&quot;&amp;gt;
        Task Management App
      &amp;lt;/h1&amp;gt;

      &amp;lt;p className=&quot;mt-4 text-gray-600&quot;&amp;gt;
        Organize your work and manage your daily tasks.
      &amp;lt;/p&amp;gt;

      &amp;lt;a
        href=&quot;/dashboard&quot;
        className=&quot;mt-6 rounded bg-blue-600 px-6 py-3 text-white&quot;
      &amp;gt;
        Open Dashboard
      &amp;lt;/a&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a basic landing page.&lt;/p&gt;
&lt;p&gt;In a production application, we could expand it with navigation, authentication links, feature descriptions, and responsive layouts.&lt;/p&gt;
&lt;p&gt;A useful principle is to keep reusable UI elements inside the &lt;code&gt;components&lt;/code&gt; directory instead of repeating the same code across multiple pages.&lt;/p&gt;
&lt;h3 id=&quot;8-implementing-crud-operations&quot;&gt;8. Implementing CRUD Operations&lt;/h3&gt;
&lt;p&gt;CRUD stands for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create&lt;/li&gt;
&lt;li&gt;Read&lt;/li&gt;
&lt;li&gt;Update&lt;/li&gt;
&lt;li&gt;Delete&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These operations form the foundation of many database-driven applications.&lt;/p&gt;
&lt;p&gt;Let's implement them using Next.js Route Handlers and Prisma.&lt;/p&gt;
&lt;h4 id=&quot;create-a-task-api&quot;&gt;Create a Task API&lt;/h4&gt;
&lt;p&gt;Create &lt;code&gt;app/api/tasks/route.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { NextResponse } from &quot;next/server&quot;;
import { prisma } from &quot;@/lib/db&quot;;

export async function GET() {
  try {
    const tasks = await prisma.task.findMany({
      orderBy: {
        createdAt: &quot;desc&quot;,
      },
    });

    return NextResponse.json(tasks);
  } catch {
    return NextResponse.json(
      { error: &quot;Failed to fetch tasks&quot; },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  try {
    const body = await request.json();

    if (
      typeof body.title !== &quot;string&quot; ||
      !body.title.trim()
    ) {
      return NextResponse.json(
        { error: &quot;A valid title is required&quot; },
        { status: 400 }
      );
    }

    const task = await prisma.task.create({
      data: {
        title: body.title.trim(),
        description: body.description ?? null,
        userId: 1,
      },
    });

    return NextResponse.json(task, { status: 201 });
  } catch {
    return NextResponse.json(
      { error: &quot;Failed to create task&quot; },
      { status: 500 }
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This demonstrates how the frontend communicates with the database through a server-side endpoint.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;userId: 1&lt;/code&gt; value is only a temporary development placeholder. A real application must obtain the authenticated user's ID from a verified server-side session and authorize every operation.&lt;/p&gt;
&lt;h4 id=&quot;reading-tasks&quot;&gt;Reading Tasks&lt;/h4&gt;
&lt;p&gt;The GET handler retrieves tasks from PostgreSQL.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;orderBy&lt;/code&gt; option sorts tasks by creation date, placing the newest records first.&lt;/p&gt;
&lt;p&gt;For a real multi-user application, the query should filter by the authenticated user's ID.&lt;/p&gt;
&lt;h4 id=&quot;updating-a-task&quot;&gt;Updating a Task&lt;/h4&gt;
&lt;p&gt;A PATCH endpoint can update an existing task.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const updatedTask = await prisma.task.update({
  where: {
    id: taskId,
  },
  data: {
    completed: true,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application should validate the task ID and verify that the authenticated user owns the task before updating it.&lt;/p&gt;
&lt;h4 id=&quot;deleting-a-task&quot;&gt;Deleting a Task&lt;/h4&gt;
&lt;p&gt;Prisma provides a simple deletion method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await prisma.task.delete({
  where: {
    id: taskId,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a production system, authorization checks are essential to prevent users from deleting other users' data.&lt;/p&gt;
&lt;h3 id=&quot;9-building-the-frontend-dashboard&quot;&gt;9. Building the Frontend Dashboard&lt;/h3&gt;
&lt;p&gt;A dashboard is where users interact with application data.&lt;/p&gt;
&lt;p&gt;A useful task dashboard should provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A task creation form&lt;/li&gt;
&lt;li&gt;A list of existing tasks&lt;/li&gt;
&lt;li&gt;Completion indicators&lt;/li&gt;
&lt;li&gt;Edit and delete buttons&lt;/li&gt;
&lt;li&gt;Loading states&lt;/li&gt;
&lt;li&gt;Error messages&lt;/li&gt;
&lt;li&gt;Empty-state guidance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The interface should remain understandable even when the database is empty or a request fails.&lt;/p&gt;
&lt;p&gt;For example, instead of showing a blank screen, display:&lt;/p&gt;
&lt;p&gt;&quot;No tasks yet. Create your first task to get started.&quot;&lt;/p&gt;
&lt;p&gt;This small detail improves usability.&lt;/p&gt;
&lt;h4 id=&quot;example-react-component&quot;&gt;Example React Component&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&quot;use client&quot;;

import { useState } from &quot;react&quot;;

export default function TaskForm() {
  const [title, setTitle] = useState(&quot;&quot;);
  const [loading, setLoading] = useState(false);

  async function handleSubmit(
    event: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;
  ) {
    event.preventDefault();

    if (!title.trim()) return;

    setLoading(true);

    try {
      const response = await fetch(&quot;/api/tasks&quot;, {
        method: &quot;POST&quot;,
        headers: {
          &quot;Content-Type&quot;: &quot;application/json&quot;,
        },
        body: JSON.stringify({ title }),
      });

      if (!response.ok) {
        throw new Error(&quot;Unable to create task&quot;);
      }

      setTitle(&quot;&quot;);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  }

  return (
    &amp;lt;form onSubmit={handleSubmit}&amp;gt;
      &amp;lt;input
        value={title}
        onChange={(event) =&amp;gt; setTitle(event.target.value)}
        placeholder=&quot;Enter task title&quot;
      /&amp;gt;

      &amp;lt;button disabled={loading}&amp;gt;
        {loading ? &quot;Saving...&quot; : &quot;Add Task&quot;}
      &amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component demonstrates controlled form input, asynchronous requests, and loading-state management.&lt;/p&gt;
&lt;p&gt;A complete dashboard would also refresh its task list after a successful submission.&lt;/p&gt;
&lt;h3 id=&quot;10-implementing-authentication&quot;&gt;10. Implementing Authentication&lt;/h3&gt;
&lt;p&gt;Authentication is a critical part of applications that store personal information.&lt;/p&gt;
&lt;p&gt;Users should only be able to access their own records.&lt;/p&gt;
&lt;p&gt;A typical authentication workflow includes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User registration&lt;/li&gt;
&lt;li&gt;Password validation&lt;/li&gt;
&lt;li&gt;Secure password hashing&lt;/li&gt;
&lt;li&gt;Login verification&lt;/li&gt;
&lt;li&gt;Session creation&lt;/li&gt;
&lt;li&gt;Protected routes&lt;/li&gt;
&lt;li&gt;Logout functionality&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Passwords must never be stored as plain text.&lt;/p&gt;
&lt;p&gt;Use a password hashing algorithm such as Argon2id or bcrypt.&lt;/p&gt;
&lt;p&gt;For a production application, a maintained authentication solution such as Auth.js can help manage sessions and authentication flows.&lt;/p&gt;
&lt;h4 id=&quot;protecting-user-data&quot;&gt;Protecting User Data&lt;/h4&gt;
&lt;p&gt;Authentication alone is not enough.&lt;/p&gt;
&lt;p&gt;Every database operation should check authorization.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const tasks = await prisma.task.findMany({
  where: {
    userId: authenticatedUser.id,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures that the query retrieves records associated with the authenticated account.&lt;/p&gt;
&lt;p&gt;The server must derive &lt;code&gt;authenticatedUser.id&lt;/code&gt; from a verified session, not from a user-controlled request body.&lt;/p&gt;
&lt;p&gt;This distinction is essential for preventing insecure direct object reference vulnerabilities.&lt;/p&gt;
&lt;h3 id=&quot;11-form-validation-and-error-handling&quot;&gt;11. Form Validation and Error Handling&lt;/h3&gt;
&lt;p&gt;Users do not always submit valid information.&lt;/p&gt;
&lt;p&gt;They may leave fields empty, enter incorrect values, or submit unexpected data.&lt;/p&gt;
&lt;p&gt;A robust application should validate data on both the client and server.&lt;/p&gt;
&lt;p&gt;Libraries such as Zod can help define reusable validation schemas.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { z } from &quot;zod&quot;;

export const taskSchema = z.object({
  title: z.string().trim().min(1).max(120),
  description: z.string().max(1000).optional(),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server should validate incoming data before interacting with PostgreSQL.&lt;/p&gt;
&lt;p&gt;Good error handling should also distinguish between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Invalid user input&lt;/li&gt;
&lt;li&gt;Authentication failures&lt;/li&gt;
&lt;li&gt;Authorization failures&lt;/li&gt;
&lt;li&gt;Missing records&lt;/li&gt;
&lt;li&gt;Database errors&lt;/li&gt;
&lt;li&gt;Unexpected server failures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avoid returning raw database errors or sensitive implementation details to users.&lt;/p&gt;
&lt;h3 id=&quot;12-improving-database-performance&quot;&gt;12. Improving Database Performance&lt;/h3&gt;
&lt;p&gt;As an application grows, database performance becomes increasingly important.&lt;/p&gt;
&lt;p&gt;A query that works perfectly with 20 records may become slow with 200,000 records.&lt;/p&gt;
&lt;p&gt;Several techniques can improve performance.&lt;/p&gt;
&lt;h4 id=&quot;indexing&quot;&gt;Indexing&lt;/h4&gt;
&lt;p&gt;PostgreSQL indexes help speed up queries that frequently filter or sort by particular fields.&lt;/p&gt;
&lt;p&gt;For example, if the application frequently retrieves tasks by user ID, an index on &lt;code&gt;userId&lt;/code&gt; may help.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_tasks_user_id
ON &quot;Task&quot; (&quot;userId&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Indexes improve some read operations but introduce storage and write overhead, so they should be selected based on actual query patterns.&lt;/p&gt;
&lt;h4 id=&quot;pagination&quot;&gt;Pagination&lt;/h4&gt;
&lt;p&gt;Avoid loading thousands of records at once.&lt;/p&gt;
&lt;p&gt;Instead, implement pagination.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const tasks = await prisma.task.findMany({
  skip: 0,
  take: 20,
  orderBy: {
    createdAt: &quot;desc&quot;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For large datasets, cursor-based pagination can provide more consistent performance.&lt;/p&gt;
&lt;h4 id=&quot;connection-pooling&quot;&gt;Connection Pooling&lt;/h4&gt;
&lt;p&gt;Production applications may handle many simultaneous requests.&lt;/p&gt;
&lt;p&gt;Database connection pooling helps control the number of active connections and avoid exhausting PostgreSQL resources.&lt;/p&gt;
&lt;p&gt;The appropriate pooling strategy depends on the deployment platform and database provider.&lt;/p&gt;
&lt;h3 id=&quot;13-testing-the-application&quot;&gt;13. Testing the Application&lt;/h3&gt;
&lt;p&gt;Testing is not an optional step in professional development.&lt;/p&gt;
&lt;p&gt;It helps identify problems before users encounter them.&lt;/p&gt;
&lt;p&gt;A complete testing strategy can include:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unit Testing:&lt;/strong&gt; Test individual functions, validation rules, and utility logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integration Testing:&lt;/strong&gt; Verify that API routes interact correctly with PostgreSQL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;End-to-End Testing:&lt;/strong&gt; Simulate user journeys such as registration, login, task creation, and deletion.&lt;/p&gt;
&lt;p&gt;Tools such as Vitest, Jest, and Playwright can support these workflows.&lt;/p&gt;
&lt;p&gt;Important scenarios to test include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating a task with valid information&lt;/li&gt;
&lt;li&gt;Rejecting an empty title&lt;/li&gt;
&lt;li&gt;Preventing unauthorized access&lt;/li&gt;
&lt;li&gt;Updating an existing task&lt;/li&gt;
&lt;li&gt;Handling a missing task&lt;/li&gt;
&lt;li&gt;Displaying errors when the database is unavailable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Testing helps make future code changes safer and easier.&lt;/p&gt;
&lt;h3 id=&quot;14-deploying-the-application&quot;&gt;14. Deploying the Application&lt;/h3&gt;
&lt;p&gt;After completing development and testing, the next step is deployment.&lt;/p&gt;
&lt;p&gt;A common deployment setup includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js application hosted on Vercel&lt;/li&gt;
&lt;li&gt;PostgreSQL hosted on a managed database platform&lt;/li&gt;
&lt;li&gt;Environment variables configured securely&lt;/li&gt;
&lt;li&gt;Database migrations applied during deployment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before deploying, verify:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Production database credentials are configured.&lt;/li&gt;
&lt;li&gt;Authentication secrets are secure.&lt;/li&gt;
&lt;li&gt;Database migrations have been applied.&lt;/li&gt;
&lt;li&gt;Sensitive environment files are excluded from Git.&lt;/li&gt;
&lt;li&gt;Error handling is configured.&lt;/li&gt;
&lt;li&gt;The application builds successfully.&lt;/li&gt;
&lt;li&gt;Database backups are available.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For production migrations, use the appropriate deployment migration command, such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma migrate deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not use development migration workflows against a live production database.&lt;/p&gt;
&lt;p&gt;After deployment, monitor application logs, database performance, and error rates.&lt;/p&gt;
&lt;h3 id=&quot;15-common-challenges-developers-face&quot;&gt;15. Common Challenges Developers Face&lt;/h3&gt;
&lt;p&gt;Even with modern tools, full stack development involves challenges.&lt;/p&gt;
&lt;h4 id=&quot;database-connection-errors&quot;&gt;Database Connection Errors&lt;/h4&gt;
&lt;p&gt;Incorrect credentials, unavailable database servers, or connection limits can cause failures.&lt;/p&gt;
&lt;p&gt;Always verify environment configuration and database connectivity.&lt;/p&gt;
&lt;h4 id=&quot;authentication-problems&quot;&gt;Authentication Problems&lt;/h4&gt;
&lt;p&gt;Improper session validation can expose private information.&lt;/p&gt;
&lt;p&gt;Use established authentication libraries and enforce authorization on the server.&lt;/p&gt;
&lt;h4 id=&quot;slow-queries&quot;&gt;Slow Queries&lt;/h4&gt;
&lt;p&gt;Unoptimized database queries can affect response times.&lt;/p&gt;
&lt;p&gt;Use query analysis, appropriate indexes, pagination, and efficient data retrieval.&lt;/p&gt;
&lt;h4 id=&quot;frontend-and-backend-mismatches&quot;&gt;Frontend and Backend Mismatches&lt;/h4&gt;
&lt;p&gt;The frontend may expect a different response structure than the backend provides.&lt;/p&gt;
&lt;p&gt;TypeScript types, validation schemas, and consistent API contracts help reduce these problems.&lt;/p&gt;
&lt;h4 id=&quot;deployment-differences&quot;&gt;Deployment Differences&lt;/h4&gt;
&lt;p&gt;An application that works locally may fail in production because of missing environment variables, migration issues, or incompatible runtime configuration.&lt;/p&gt;
&lt;p&gt;Testing the production build before deployment helps identify these issues earlier.&lt;/p&gt;
&lt;h3 id=&quot;16-how-to-extend-this-project&quot;&gt;16. How to Extend This Project&lt;/h3&gt;
&lt;p&gt;Once the basic task management application is complete, developers can add more advanced features.&lt;/p&gt;
&lt;h4 id=&quot;role-based-access-control&quot;&gt;Role-Based Access Control&lt;/h4&gt;
&lt;p&gt;Introduce roles such as administrator, manager, and standard user.&lt;/p&gt;
&lt;p&gt;Each role can have different permissions.&lt;/p&gt;
&lt;h4 id=&quot;file-uploads&quot;&gt;File Uploads&lt;/h4&gt;
&lt;p&gt;Allow users to attach documents or images to tasks using an object storage service.&lt;/p&gt;
&lt;h4 id=&quot;real-time-updates&quot;&gt;Real-Time Updates&lt;/h4&gt;
&lt;p&gt;Use WebSockets or a suitable real-time service to synchronize task changes between users.&lt;/p&gt;
&lt;h4 id=&quot;email-notifications&quot;&gt;Email Notifications&lt;/h4&gt;
&lt;p&gt;Send reminders when task deadlines approach.&lt;/p&gt;
&lt;h4 id=&quot;analytics-dashboard&quot;&gt;Analytics Dashboard&lt;/h4&gt;
&lt;p&gt;Display completed tasks, pending tasks, productivity trends, and activity summaries.&lt;/p&gt;
&lt;h4 id=&quot;ai-powered-features&quot;&gt;AI-Powered Features&lt;/h4&gt;
&lt;p&gt;Integrate an AI service to summarize tasks, suggest priorities, or convert natural-language instructions into structured task descriptions.&lt;/p&gt;
&lt;p&gt;These extensions transform a basic CRUD project into a more complete software product.&lt;/p&gt;
&lt;h3 id=&quot;17-lessons-learned-from-building-with-next-js-and-postgresql&quot;&gt;17. Lessons Learned from Building with Next.js and PostgreSQL&lt;/h3&gt;
&lt;p&gt;Building a full stack application teaches several important engineering principles.&lt;/p&gt;
&lt;p&gt;First, application architecture matters. Separating database access, validation, UI components, and authentication makes code easier to understand.&lt;/p&gt;
&lt;p&gt;Second, data security must be considered from the beginning rather than added at the end.&lt;/p&gt;
&lt;p&gt;Third, a working application is not necessarily a production-ready application. Performance, testing, monitoring, accessibility, and deployment all contribute to reliability.&lt;/p&gt;
&lt;p&gt;Finally, choosing technologies is only one part of software engineering. Understanding how they interact is what allows developers to build useful products.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Next.js and PostgreSQL provide a practical foundation for building modern full stack applications.&lt;/p&gt;
&lt;p&gt;Next.js simplifies routing, rendering, and server-side application logic, while PostgreSQL provides reliable relational data storage.&lt;/p&gt;
&lt;p&gt;By combining these technologies with Prisma, TypeScript, secure authentication, validation, and thoughtful database design, developers can create applications that are easier to maintain and extend.&lt;/p&gt;
&lt;p&gt;Our task management example demonstrates the essential building blocks: project setup, database modeling, CRUD operations, frontend development, authentication, performance optimization, testing, and deployment.&lt;/p&gt;
&lt;p&gt;Whether you are a beginner learning full stack development or an experienced developer expanding your technical portfolio, building a complete application from start to finish is a valuable way to strengthen your engineering skills.&lt;/p&gt;
&lt;p&gt;The real achievement is not simply writing code that runs. It is creating a secure, reliable, and useful application that solves an actual problem.&lt;/p&gt;
&lt;p&gt;**What would you build first with Next.js and PostgreSQL: a task management system, an ecommerce platform, or a SaaS dashboard? Share your thoughts and project ideas in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29558/next-js-postgresql-build-a-complete-full-stack-application</guid>
<pubDate>Fri, 02 Oct 2026 15:53:08 +0000</pubDate>
</item>
<item>
<title>PostgreSQL with pgvector vs Vector DBs: Why Almost Nobody Needs Pinecone</title>
<link>https://coderlegion.com/28109/postgresql-with-pgvector-vs-vector-dbs-why-almost-nobody-needs-pinecone</link>
<description>&lt;p&gt;Your team just signed up for a dedicated vector database costing &lt;strong&gt;$300 per month&lt;/strong&gt; to index 50,000 customer support documents. The dashboard looks sleek, the documentation promises scalability to billions of vectors, and the product team celebrates that you are now officially &quot;AI-native.&quot;&lt;/p&gt;
&lt;p&gt;Yet in the shadows of your infrastructure, an operational nightmare has just been born: you now have &lt;strong&gt;two sources of truth&lt;/strong&gt;. Every time a user updates a document in your primary relational database, an asynchronous synchronization job must push the update to the external vector database. If that sync job fails in the middle of the night, your RAG pipeline serves outdated or nonexistent information. You have broken ACID transactional consistency, doubled your storage overhead, added network latency to every query, and fragmented your security model.&lt;/p&gt;
&lt;p&gt;All of this to index 50,000 vectors that &lt;strong&gt;your existing PostgreSQL instance could query in 8 milliseconds with a single line of SQL and zero additional cost&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In line with the engineering decisions we have championed across this blog — from separation of concerns in &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/rag_antipatterns/&quot;&gt;RAG: 7 Anti-Patterns&lt;/a&gt; to the pragmatic simplicity of our &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/productivity_stack_2026/&quot;&gt;Productivity Stack 2026&lt;/a&gt; with Supabase —, this article breaks down the most polarizing data infrastructure debate in applied AI: &lt;strong&gt;when do you genuinely need a specialized Vector DB (Pinecone, Qdrant, Milvus, Weaviate), and when is PostgreSQL with &lt;code&gt;pgvector&lt;/code&gt; the vastly superior engineering choice?&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-vector-database-gold-rush&quot;&gt;The Vector Database Gold Rush&lt;/h4&gt;
&lt;p&gt;To understand how we arrived here, we must look back at the generative AI explosion of 2023–2024. When software developers discovered that converting text into multidimensional numerical representations (&lt;em&gt;embeddings&lt;/em&gt;) enabled semantic search via geometric proximity, an immediate infrastructure question arose: where do we store and query these 1,536-dimensional vectors?&lt;/p&gt;
&lt;p&gt;A wave of deep-tech startups raised hundreds of millions in venture capital, promising specialized vector search engines engineered from scratch for linear algebra and Approximate Nearest Neighbor (ANN) search. Pinecone, Qdrant, Chroma, Weaviate, and Milvus were born.&lt;/p&gt;
&lt;p&gt;These dedicated vector databases did an extraordinary job evangelizing semantic search across the industry. But they made a fatal foundational assumption: &lt;strong&gt;they assumed that battle-tested relational database engines would be too slow to adapt to the AI era&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;They were entirely wrong. In the open-source ecosystem, the &lt;strong&gt;&lt;code&gt;pgvector&lt;/code&gt;&lt;/strong&gt; extension transformed PostgreSQL — the most robust, mature, and widely deployed database engine on earth — into a world-class vector search engine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/pgvector_vs_vectordb/unified_architecture.jpg&quot; alt=&quot;Unified database architecture with pgvector versus fragmented dual-database architecture&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-hidden-cost-of-dual-database-architecture&quot;&gt;The Hidden Cost of Dual-Database Architecture&lt;/h4&gt;
&lt;p&gt;Introducing a dedicated vector database is never just another monthly line item on your cloud bill. It is an &lt;strong&gt;architectural coupling decision&lt;/strong&gt; that introduces four critical points of failure:&lt;/p&gt;
&lt;h5 id=&quot;1-the-dual-write-problem&quot;&gt;1. The Dual-Write Problem&lt;/h5&gt;
&lt;p&gt;When business data lives in two disconnected systems (your primary SQL database and your external Vector DB), every data mutation must write to both. What happens if the write to PostgreSQL succeeds, but the API call to Pinecone times out? State drifts out of sync immediately. To fix this, engineering teams are forced to build distributed event queues (Kafka, RabbitMQ), Outbox patterns, or complex Change Data Capture (CDC) pipelines, adding hundreds of lines of glue code and failure points.&lt;/p&gt;
&lt;h5 id=&quot;2-loss-of-acid-transactions&quot;&gt;2. Loss of ACID Transactions&lt;/h5&gt;
&lt;p&gt;In PostgreSQL, a &lt;code&gt;BEGIN ... COMMIT&lt;/code&gt; block guarantees that operations are atomic, consistent, isolated, and durable. Storing vectors inside the same table or in a foreign-key relation in PostgreSQL ensures that deleting a document and deleting its embedding happen in the exact same atomic transaction. With an external Vector DB, eventual consistency is the absolute best you can achieve.&lt;/p&gt;
&lt;h5 id=&quot;3-distributed-network-latency&quot;&gt;3. Distributed Network Latency&lt;/h5&gt;
&lt;p&gt;A realistic RAG query rarely searches raw vectors alone; it filters by relational metadata: &lt;em&gt;&quot;find the most relevant chunks from the technical manual, but only for version 2.4, created after January 2026, and belonging to user X's tenant.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In a split architecture, the query flow is tortuous:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your backend queries PostgreSQL to retrieve the authorized tenant document IDs.&lt;/li&gt;
&lt;li&gt;Your backend sends those IDs over the public internet as a filter payload to the external Vector DB (adding 50–150 ms of network latency).&lt;/li&gt;
&lt;li&gt;The Vector DB executes vector similarity and returns chunk IDs.&lt;/li&gt;
&lt;li&gt;Your backend round-trips back to PostgreSQL to retrieve the full raw text and relational metadata.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With &lt;code&gt;pgvector&lt;/code&gt;, this entire workflow resolves in &lt;strong&gt;a single SQL query within the exact same database process&lt;/strong&gt;.&lt;/p&gt;
&lt;h5 id=&quot;4-fragmented-security-and-row-level-security-rls-&quot;&gt;4. Fragmented Security and Row Level Security (RLS)&lt;/h5&gt;
&lt;p&gt;As we explored in &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/prompt_injection/&quot;&gt;Prompt Injection&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/eu_ai_act/&quot;&gt;EU AI Act&lt;/a&gt;, Row Level Security (RLS) is a non-negotiable defensive barrier for multi-tenant applications. In Supabase PostgreSQL, native RLS policies ensure that a user can never retrieve embeddings belonging to another organization, because authorization is enforced directly inside the database kernel. With an external Vector DB, you must replicate and maintain complex authorization logic across multiple application layers.&lt;/p&gt;
&lt;h4 id=&quot;inside-pgvector-mechanics-hnsw-vs-ivfflat&quot;&gt;Inside pgvector Mechanics: HNSW vs IVFFlat&lt;/h4&gt;
&lt;p&gt;To operate &lt;code&gt;pgvector&lt;/code&gt; in production with engineering confidence, you must understand how it indexes and traverses vector spaces. &lt;code&gt;pgvector&lt;/code&gt; supports the industry's two dominant indexing algorithms:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Enable the pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Create a table with a 1536-dimensional vector column (OpenAI / Vertex AI)
CREATE TABLE document_sections (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    article_slug TEXT NOT NULL,
    section_title TEXT NOT NULL,
    content TEXT NOT NULL,
    category TEXT NOT NULL,
    embedding VECTOR(1536),
    created_at TIMESTAMPTZ DEFAULT NOW()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;1-ivfflat-index-inverted-file-flat-&quot;&gt;1. IVFFlat Index (Inverted File Flat)&lt;/h5&gt;
&lt;p&gt;IVFFlat partitions the high-dimensional vector space into $K$ clusters or inverted lists using k-means clustering. At query time, the search algorithm identifies the nearest centroids and only scans vectors residing in those selected lists.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Fast build times and minimal RAM utilization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Requires existing data before creating the index so centroids can be accurately calculated. Lower recall in high-dimensional spaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;2-hnsw-index-hierarchical-navigable-small-world-&quot;&gt;2. HNSW Index (Hierarchical Navigable Small World)&lt;/h5&gt;
&lt;p&gt;HNSW constructs a multi-layered hierarchical graph where vertices represent vectors and edges connect near neighbors. Search starts at the top layer with broad hops and descends into denser layers for high-precision local search.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Create an HNSW index optimized for cosine similarity distance
CREATE INDEX ON document_sections 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Outstanding search recall (&amp;gt;98%), ultra-fast query execution (a few milliseconds), and dynamic index building: new vectors can be inserted in real time without retraining.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Higher memory consumption and longer index build times compared to IVFFlat.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the vast majority of production workloads, &lt;strong&gt;HNSW is the default recommended choice&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;the-superpower-hybrid-sql-vector-queries&quot;&gt;The Superpower: Hybrid SQL + Vector Queries&lt;/h4&gt;
&lt;p&gt;The most decisive advantage of &lt;code&gt;pgvector&lt;/code&gt; over any standalone vector database is the ability to combine full relational algebra, JSONB operators, temporal filters, and vector similarity within a single, unified SQL statement:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Hybrid query in Supabase / PostgreSQL
SELECT 
    id,
    section_title,
    content,
    1 - (embedding &amp;lt;=&amp;gt; $1) AS cosine_similarity
FROM document_sections
WHERE category = 'Artificial Intelligence'
  AND created_at &amp;gt;= NOW() - INTERVAL '6 months'
ORDER BY embedding &amp;lt;=&amp;gt; $1
LIMIT 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; operator computes cosine distance in native C directly at the CPU level. In a single execution plan, PostgreSQL applies the relational predicate filter (&lt;code&gt;category&lt;/code&gt; and timestamp) and performs nearest-neighbor search across the filtered subset, returning results in &lt;strong&gt;under 10 milliseconds&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Replicating this in a standalone Vector DB requires synchronizing all metadata, dealing with pre-filtering or post-filtering trade-offs that hurt recall, and paying the latency tax of multiple network hops.&lt;/p&gt;
&lt;h4 id=&quot;when-you-actually-need-a-dedicated-vector-db&quot;&gt;When You ACTUALLY Need a Dedicated Vector DB&lt;/h4&gt;
&lt;p&gt;Engineering integrity requires acknowledging when specialized tools outperform general-purpose engines. These are the specific architectural scenarios where a dedicated vector database (Qdrant, Milvus, Pinecone) is genuinely justified:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Scenario &lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Use PostgreSQL (&lt;code&gt;pgvector&lt;/code&gt;) &lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Use Dedicated Vector DB&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Vector Volume&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&amp;lt; 10 million vectors &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;&amp;gt; 50–100 million vectors&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Data Architecture&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Monolith or service with existing relational DB &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Massive decoupled search infrastructure&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Required Filtering&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Complex (JOINs, JSONB, RLS, relational permissions) &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Simple (basic key-value tag filters)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Hardware / Memory&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Standard server with balanced RAM/SSD &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Specialized cluster optimized purely for RAM/GPU&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Operational Overhead&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;$0 extra (included in your PostgreSQL instance) &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;$100 – $2,000+/mo for managed clusters&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Massive Horizontal Sharding&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Standard PostgreSQL table partitioning &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Native distributed sharding across dozens of nodes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Unless your company is indexing the entire Amazon product catalog or billions of posts from a global social network, &lt;strong&gt;you are well within pgvector territory&lt;/strong&gt;. For 95% of enterprise SaaS applications, internal RAG systems, and AI agent platforms, PostgreSQL handles vector workloads effortlessly.&lt;/p&gt;
&lt;h4 id=&quot;real-case-study-the-ops-copilot-on-supabase&quot;&gt;Real Case Study: The Ops Copilot on Supabase&lt;/h4&gt;
&lt;p&gt;In our &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/productivity_stack_2026/&quot;&gt;Productivity Stack 2026&lt;/a&gt;, we documented how we run Datalaria's infrastructure on Supabase (managed PostgreSQL).&lt;/p&gt;
&lt;p&gt;When we built the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/ai_agents_part8/&quot;&gt;Ops Engineering Copilot&lt;/a&gt; to enable readers to semantically query over 70 blog posts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Each article is chunked into logical markdown sections (avoiding &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/rag_antipatterns/&quot;&gt;RAG Anti-Pattern 1&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Embeddings are generated using Gemini / Vertex AI (&lt;code&gt;text-embedding-004&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Vectors and text are stored directly in a PostgreSQL table with an HNSW index via &lt;code&gt;pgvector&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When a user asks a question, an RPC stored procedure executes cosine similarity and returns enriched context in &lt;strong&gt;less than 12 ms&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Total extra infrastructure cost: &lt;strong&gt;$0&lt;/strong&gt;. Zero sync pipelines. Zero extra servers to monitor. 100% transactional integrity.&lt;/p&gt;
&lt;h4 id=&quot;connection-with-ai-economics-and-the-eu-ai-act&quot;&gt;Connection with AI Economics and the EU AI Act&lt;/h4&gt;
&lt;p&gt;This architecture aligns directly with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/hidden_economics_ai/&quot;&gt;10x Rule&lt;/a&gt; from &lt;em&gt;The Hidden Economics of AI&lt;/em&gt;: &lt;strong&gt;never adopt an external tool that adds operational friction and recurring costs unless it delivers a 10x better outcome&lt;/strong&gt;. A dedicated vector database does not deliver a 10x better result for a corpus of 100,000 documents; it delivers the exact same semantic recall with 300% more technical debt.&lt;/p&gt;
&lt;p&gt;Furthermore, under the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/eu_ai_act/&quot;&gt;EU AI Act&lt;/a&gt; (Article 10 on data governance and Article 12 on auditability), maintaining business records, user permissions, and embeddings in a unified database radically simplifies compliance audits. You don't have to document how data travels between separate vendors, nor do you struggle with GDPR &lt;em&gt;Right to be Forgotten&lt;/em&gt; requests: a simple &lt;code&gt;DELETE FROM users WHERE id = X&lt;/code&gt; cascades immediately to wipe the user, their documents, and all associated embeddings in one atomic transaction.&lt;/p&gt;
&lt;h4 id=&quot;conclusion-the-beauty-of-engineering-simplicity&quot;&gt;Conclusion: The Beauty of Engineering Simplicity&lt;/h4&gt;
&lt;p&gt;Modern software engineering suffers from a chronic temptation to collect specialized databases like trading cards. Every new paradigm seems to demand a new database, a new framework, and a new SaaS subscription.&lt;/p&gt;
&lt;p&gt;Yet true engineering elegance is never about accumulating complexity; it is about achieving &lt;strong&gt;maximum capability with the minimum failure surface&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;PostgreSQL has evolved continuously for over 30 years. It absorbed JSON (eliminating document databases for most use cases), absorbed geospatial data with PostGIS, and with &lt;code&gt;pgvector&lt;/code&gt;, it has completely absorbed modern vector search.&lt;/p&gt;
&lt;p&gt;Before opening your company credit card for another managed vector service, open a terminal to your PostgreSQL instance, run &lt;code&gt;CREATE EXTENSION vector;&lt;/code&gt;, and test it yourself. The simplest solution is almost always the most resilient.&lt;/p&gt;
&lt;hr&gt;
&lt;h5 id=&quot;sources-of-interest-&quot;&gt;Sources of Interest:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/pgvector/pgvector&quot;&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: pgvector — Open-Source Vector Similarity Search for PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://supabase.com/docs/guides/database/extensions/pgvector&quot;&gt;&lt;strong&gt;Supabase Docs&lt;/strong&gt;: Vector Columns and HNSW Indexing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://aws.amazon.com/blogs/database/optimize-hnsw-indexing-in-postgresql-with-pgvector/&quot;&gt;&lt;strong&gt;Jonathan Katz (AWS)&lt;/strong&gt;: How to Optimize HNSW Indexing in PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/rag_antipatterns/&quot;&gt;&lt;strong&gt;Datalaria&lt;/strong&gt;: RAG in Production — 7 Anti-Patterns That Destroy Precision&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/productivity_stack_2026/&quot;&gt;&lt;strong&gt;Datalaria&lt;/strong&gt;: An Engineer's Productivity Stack in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/finetuning_vs_rag/&quot;&gt;&lt;strong&gt;Datalaria&lt;/strong&gt;: Fine-Tuning vs Prompt Engineering vs RAG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/hidden_economics_ai/&quot;&gt;&lt;strong&gt;Datalaria&lt;/strong&gt;: The Hidden Economics of AI — Real Production Costs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/eu_ai_act/&quot;&gt;&lt;strong&gt;Datalaria&lt;/strong&gt;: EU AI Act — Data Governance and Traceability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28109/postgresql-with-pgvector-vs-vector-dbs-why-almost-nobody-needs-pinecone</guid>
<pubDate>Tue, 22 Sep 2026 06:24:46 +0000</pubDate>
</item>
<item>
<title>Project LifeOps (Part 3): Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanba</title>
<link>https://coderlegion.com/27227/project-lifeops-part-core-interactive-modules-fitness-library-cinema-professional-kanba</link>
<description>&lt;p&gt;In earlier installments, we engineered the foundational backbone of &lt;strong&gt;LifeOps&lt;/strong&gt;: in &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Part 1&lt;/a&gt; we designed the polymorphic PostgreSQL schema and modular FastAPI backend, and in &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;Part 2&lt;/a&gt; we brought the &lt;em&gt;Glassmorphism Dark Mode&lt;/em&gt; design system and global 360° Dashboard to life.&lt;/p&gt;
&lt;p&gt;Now comes the most defining phase of development: &lt;strong&gt;the day-to-day functional modules&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A personal operating system fails if it forces users into rigid input forms or scatters daily data across disconnected views. The challenge was articulating three completely disparate life areas under unified design and performance standards:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🏃 &lt;strong&gt;Health &amp;amp; Physical Fitness&lt;/strong&gt;: Workout logging across sports with dynamic metrics (distance volume, calories, pace calculations, and Personal Bests).&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Intellectual Culture &amp;amp; Media&lt;/strong&gt;: Book tracking with interactive reading progress bars and an entertainment catalog organized by streaming platform.&lt;/li&gt;
&lt;li&gt;💼 &lt;strong&gt;Professional Execution&lt;/strong&gt;: An agile 4-column Kanban board mapped to projects, equipped with a &lt;strong&gt;chronological progress log powered by PostgreSQL JSONB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;Test the live app&lt;/strong&gt;: You can explore the production build of LifeOps directly at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;https://datalaria.com/apps/lifeops/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-lifeops-series-roadmap&quot;&gt;🗺️ LifeOps Series Roadmap&lt;/h4&gt;
&lt;p&gt;To understand how every architectural layer fits together, this series spans 5 structured installments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Personal Operating System Architecture and FastAPI + Supabase Backend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 3 (This article)&lt;/strong&gt;: Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;In-Memory Word Dossiers (.docx) &amp;amp; Multi-Sheet Excel Engine (.xlsx)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 5&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part5_deploy_mobile_pwa/&quot;&gt;24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-the-ux-dilemma-dual-view-mode-inspiring-cards-vs-synthesized-table-&quot;&gt;1. The UX Dilemma: Dual View Mode (Inspiring Cards vs. Synthesized Table) 🎴📊&lt;/h4&gt;
&lt;p&gt;When building data-intensive dashboards, designers face a classic user experience tradeoff:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Grid / Cards View&lt;/strong&gt;: Visually engaging, spacious, and inspiring. It highlights book covers, movie posters, and workout chips at a glance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Data Table View&lt;/strong&gt;: Irreplaceable when auditing timestamps, comparing heart rates, or scanning through an entire month of logs without endless vertical scrolling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of forcing a single opinionated view, I built a &lt;strong&gt;persistent dual-mode toggle stored in &lt;code&gt;localStorage&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const [viewMode, setViewMode] = useState(() =&amp;gt; {
  return localStorage.getItem('lifeops_view_sport') || 'grid';
});

const handleViewChange = (mode) =&amp;gt; {
  setViewMode(mode);
  localStorage.setItem('lifeops_view_sport', mode);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the header of each module, two unobtrusive buttons with &lt;code&gt;LayoutGrid&lt;/code&gt; and &lt;code&gt;Table&lt;/code&gt; icons toggle the presentation mode instantly. Thanks to React and Vite's build architecture, view switches execute in sub-milliseconds without triggering redundant server roundtrips.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_sport_table_en.png&quot; alt=&quot;Dual view mode in LifeOps: Synthesized data table with key metrics, personal bests, and instant sorting&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-fitness-amp-performance-module-sportmodule-jsx-&quot;&gt;2. Fitness &amp;amp; Performance Module (&lt;code&gt;SportModule.jsx&lt;/code&gt;) 🏃💨&lt;/h4&gt;
&lt;p&gt;The fitness module caters to multi-sport athletes practicing running, road cycling, gym strength sessions, and swimming.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_sport_performance_en.png&quot; alt=&quot;Fitness and Performance Module featuring aggregate KPI summary widgets and detailed workout cards&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│  🏃 Sport &amp;amp; Fitness                    [ + Log Workout ]       [⊞|≡]   │
├─────────────────┬─────────────────┬─────────────────┬──────────────────┤
│ TOTAL DISTANCE  │ ACTIVE TIME     │ CALORIES        │ PERSONAL BESTS   │
│   142.5 km      │   18.4 hours    │   12,450 kcal   │    4 PBs 🏅      │
├─────────────────┴─────────────────┴─────────────────┴──────────────────┤
│                                                                        │
│ ┌───────────────────────────┐         ┌──────────────────────────────┐ │
│ │ 🏃 RUNNING         🏅 PB  │         │ 🚴 CYCLING                   │ │
│ │ Sunday Long Run           │         │ Mountain Pass Century Ride   │ │
│ │ 2026-09-10                │         │ 2026-09-08                   │ │
│ │ [📍 21.1 km] [⏱️ 1h 48m]  │         │ [📍 65.0 km] [⏱️ 2h 45m]     │ │
│ │ [🔥 1,420 kcal] [❤️ 152bpm]│        │ [🔥 1,890 kcal] [⛰️ +950m]    │ │
│ └───────────────────────────┘         └──────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;2-1-aggregated-high-level-kpis&quot;&gt;2.1. Aggregated High-Level KPIs&lt;/h5&gt;
&lt;p&gt;Before rendering individual activity items, we compute cumulative totals using in-memory array reducers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const totalKm = workouts.reduce((sum, act) =&amp;gt; sum + (act.workout?.distance_km || 0), 0);
const totalMinutes = workouts.reduce((sum, act) =&amp;gt; sum + (act.duration_minutes || 0), 0);
const totalCalories = workouts.reduce((sum, act) =&amp;gt; sum + (act.workout?.calories || 0), 0);
const pbCount = workouts.filter((act) =&amp;gt; act.workout?.personal_best).length;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;2-2-segregated-fastapi-payload&quot;&gt;2.2. Segregated FastAPI Payload&lt;/h5&gt;
&lt;p&gt;When an athlete records an exercise, the client dispatches a structured contract that FastAPI parses into the master table &lt;code&gt;lifeops.activities&lt;/code&gt; and the child table &lt;code&gt;lifeops.workouts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const payload = {
  activity: {
    activity_type: 'sport',
    title: formData.title,
    date: formData.date,
    duration_minutes: parseInt(formData.duration_minutes),
    description: formData.notes,
  },
  workout: {
    workout_type: formData.workout_type, // 'running' | 'cycling' | 'gym'
    distance_km: parseFloat(formData.distance_km) || null,
    calories: parseInt(formData.calories) || null,
    avg_heart_rate: parseInt(formData.avg_heart_rate) || null,
    elevation_m: parseInt(formData.elevation_m) || null,
    personal_best: formData.personal_best,
    notes: formData.notes,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures shared attributes (dates, general durations, notes) remain globally indexable for the 360° Dashboard, while biometric attributes (elevation gain, heart rate, personal bests) reside in their specialized child table with cascading foreign key guarantees.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-culture-amp-media-tracking-books-amp-cinema-&quot;&gt;3. Culture &amp;amp; Media Tracking: Books &amp;amp; Cinema 📚🎬&lt;/h4&gt;
&lt;p&gt;For cultural tracking, the goal was leaving behind fragmented mobile notes in favor of an enriched visual library.&lt;/p&gt;
&lt;h5 id=&quot;3-1-interactive-reading-progress-booksmodule-jsx-&quot;&gt;3.1. Interactive Reading Progress (&lt;code&gt;BooksModule.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;On each book card, we calculate page completion percentages in real time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const percentage = b.pages_total &amp;gt; 0 
  ? Math.min(100, Math.round((b.pages_read / b.pages_total) * 100)) 
  : 0;

&amp;lt;div className=&quot;book-progress-wrapper&quot;&amp;gt;
  &amp;lt;div className=&quot;progress-info&quot;&amp;gt;
    &amp;lt;span&amp;gt;{b.pages_read} / {b.pages_total} pages&amp;lt;/span&amp;gt;
    &amp;lt;span className=&quot;percentage-tag&quot;&amp;gt;{percentage}%&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div className=&quot;progress-bar-bg&quot;&amp;gt;
    &amp;lt;div 
      className=&quot;progress-bar-fill&quot; 
      style={{ 
        width: `${percentage}%`,
        background: percentage === 100 ? 'var(--accent-emerald)' : 'var(--grad-primary)'
      }} 
    /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A status filter organizes titles into &lt;code&gt;reading&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, and &lt;code&gt;wishlist&lt;/code&gt;, paired with an interactive 5-star rating system using &lt;code&gt;lucide-react&lt;/code&gt; icons.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_books_progress_en.png&quot; alt=&quot;Reading Library module with dynamic progress tracking percentages and status badges&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;3-2-streaming-media-catalog-filmsmodule-jsx-&quot;&gt;3.2. Streaming Media Catalog (&lt;code&gt;FilmsModule.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;The cinema module tracks movies, TV series, and documentaries cataloged by streaming provider (&lt;em&gt;Netflix, HBO Max, Prime Video, Disney+, Cinema&lt;/em&gt;). Each entry logs release year, director, rating, and personal review notes, answering: &lt;em&gt;&quot;What were the best films I watched this past quarter?&quot;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_films_catalog_en.png&quot; alt=&quot;Cinema and TV series catalog organized by streaming provider with ratings&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-professional-execution-the-kanban-board-kanbanboard-jsx-&quot;&gt;4. Professional Execution: The Kanban Board (&lt;code&gt;KanbanBoard.jsx&lt;/code&gt;) 📋&lt;/h4&gt;
&lt;p&gt;For professional tasks, I implemented a full Kanban board featuring 4 columns reflecting agile task lifecycles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;📝 &lt;strong&gt;To Do (&lt;code&gt;todo&lt;/code&gt;)&lt;/strong&gt;: Backlog tasks queued for upcoming execution.&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;In Progress (&lt;code&gt;in_progress&lt;/code&gt;)&lt;/strong&gt;: Active work during the current sprint.&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;In Review (&lt;code&gt;review&lt;/code&gt;)&lt;/strong&gt;: Tasks pending review, testing, or third-party feedback.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Done (&lt;code&gt;done&lt;/code&gt;)&lt;/strong&gt;: Completed deliverables.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/lifeops_kanban_board_en.png&quot; alt=&quot;Professional 4-column Kanban board with project tags, urgent priority chips, and progress badges&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ 📝 TO DO (3)     │ ⚡ IN PROGRESS (2)│ 🔍 IN REVIEW (1) │ ✅ DONE (8)      │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ ┌──────────────┐ │ ┌──────────────┐ │ ┌──────────────┐ │ ┌──────────────┐ │
│ │ Refactor API │ │ │ Frontend UI  │ │ │ E2E Test Run │ │ │ DB Migration │ │
│ │ 🚨 Critical  │ │ │ 🟡 Medium    │ │ │ 🟢 Low       │ │ │ 2026-09-02   │ │
│ │ 📅 Sep 18    │ │ │ 💬 3 notes   │ │ │ 📅 Today     │ │ │ 💬 5 notes   │ │
│ │ [→ Move]     │ │ │ [←]      [→] │ │ │ [←]      [→] │ │ │ [← Move]     │ │
│ └──────────────┘ │ └──────────────┘ │ └──────────────┘ │ └──────────────┘ │
└──────────────────┴──────────────────┴──────────────────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;4-1-optimistic-ui-updates-for-tactile-fluidity&quot;&gt;4.1. Optimistic UI Updates for Tactile Fluidity&lt;/h5&gt;
&lt;p&gt;When moving a task from &lt;code&gt;in_progress&lt;/code&gt; to &lt;code&gt;review&lt;/code&gt;, waiting for the entire roundtrip (browser → Render API → Supabase → response) introduces an undesirable delay of several hundred milliseconds.&lt;/p&gt;
&lt;p&gt;To deliver an instantaneous tactile feel, we execute an &lt;strong&gt;optimistic state update&lt;/strong&gt; in React:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const handleMoveTask = async (taskId, currentStatus, direction) =&amp;gt; {
  const statusOrder = ['todo', 'in_progress', 'review', 'done'];
  const currentIndex = statusOrder.indexOf(currentStatus);
  const targetIndex = direction === 'next' ? currentIndex + 1 : currentIndex - 1;

  if (targetIndex &amp;lt; 0 || targetIndex &amp;gt;= statusOrder.length) return;
  const targetStatus = statusOrder[targetIndex];

  // 1. Instantaneous optimistic update
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27227/project-lifeops-part-core-interactive-modules-fitness-library-cinema-professional-kanba</guid>
<pubDate>Tue, 15 Sep 2026 06:12:43 +0000</pubDate>
</item>
<item>
<title>Project LifeOps (Part 1): Personal Operating System Architecture and FastAPI + Supabase Backend</title>
<link>https://coderlegion.com/26753/project-lifeops-part-personal-operating-system-architecture-fastapi-supabase-backend</link>
<description>&lt;p&gt;How many apps do you use daily to manage your life? If you are anything like me, you probably have one app to track your runs and gym workouts, another to keep tabs on books, a website to rate movies and TV series, and two or three more tools for project management, tasks, and notes.&lt;/p&gt;
&lt;p&gt;The result is almost always the same: &lt;strong&gt;scattered data silos, mounting monthly subscription bills, and zero ability to cross-analyze data or generate a consolidated monthly executive review&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With that problem in mind and true to the Datalaria ethos of &quot;learning by building&quot;, I decided to design and engineer &lt;strong&gt;LifeOps&lt;/strong&gt;: an &lt;strong&gt;All-in-One Personal &amp;amp; Professional Operating System (OS)&lt;/strong&gt; that unifies under a single responsive dashboard:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🏃 &lt;strong&gt;Fitness &amp;amp; Performance&lt;/strong&gt;: Track workouts (Running, Cycling, Gym), distance volume, Personal Bests (PB), and calories.&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Reading &amp;amp; Culture&lt;/strong&gt;: Book progress tracking, percentage bars, and ratings.&lt;/li&gt;
&lt;li&gt;🎬 &lt;strong&gt;Cinema &amp;amp; Entertainment&lt;/strong&gt;: Movie, TV series, and documentary catalog organized by platform.&lt;/li&gt;
&lt;li&gt;📋 &lt;strong&gt;Professional Portfolio &amp;amp; Tasks&lt;/strong&gt;: Interactive Kanban board, budget tracking, and deliverable milestones.&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Reporting &amp;amp; Data Portability&lt;/strong&gt;: Streaming Word (&lt;code&gt;.docx&lt;/code&gt;) executive dossiers, multi-sheet Excel workbooks (&lt;code&gt;.xlsx&lt;/code&gt;), and raw CSV exports with UTF-8 BOM.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Best of all: &lt;strong&gt;built on top of a rock-solid, production-grade architecture that is 100% Free ($0/month)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this first installment of the series, we dive into the core foundations: &lt;strong&gt;cloud architecture design&lt;/strong&gt;, &lt;strong&gt;PostgreSQL relational data modeling&lt;/strong&gt;, and building the &lt;strong&gt;high-performance FastAPI backend&lt;/strong&gt;. Let's get into it! 🚀&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;Test the live app&lt;/strong&gt;: You can explore the production build of LifeOps directly at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;https://datalaria.com/apps/lifeops/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-lifeops-series-roadmap-1&quot;&gt;🗺️ LifeOps Series Roadmap&lt;/h4&gt;
&lt;p&gt;To understand how every architectural layer fits together, this series spans 5 structured installments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 1 (This article)&lt;/strong&gt;: Personal Operating System Architecture and FastAPI + Supabase Backend.&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 3&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;In-Memory Word Dossiers (.docx) &amp;amp; Multi-Sheet Excel Engine (.xlsx)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 5&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part5_deploy_mobile_pwa/&quot;&gt;24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-0-month-architecture-how-to-run-free-at-scale-&quot;&gt;The $0/Month Architecture: How to Run Free at Scale 💡&lt;/h4&gt;
&lt;p&gt;When architecting LifeOps, the goal wasn't merely to run on &lt;code&gt;localhost&lt;/code&gt;, but to deploy a production-grade system with zero recurring hosting costs using the most generous free tiers available:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                       CLIENT BROWSER                        │
│            [https://datalaria.com/apps/lifeops](https://datalaria.com/apps/lifeops)               │
└──────────────────────────────┬──────────────────────────────┘
                               │ (Proxy Rewrite 200)
                               ▼
┌─────────────────────────────────────────────────────────────┐
│               FRONTEND: Netlify CDN ($0/month)              │
│          React 18 + Vite 8 + i18n (ES/EN) + CSS Glass       │
└──────────────────────────────┬──────────────────────────────┘
                               │
                JWT Auth Token │  REST API Calls / Streaming
                               ▼
┌─────────────────────────────────────────────────────────────┐
│               BACKEND: FastAPI + Python 3.13                │
│    • In-Memory Docx Generator (python-docx + io.BytesIO)    │
│    • Multi-Sheet Excel Engine (openpyxl)                    │
│    • Anti-DoS Rate Limiting (slowapi) + Strict CORS         │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│              DATABASE: Supabase Managed Cloud               │
│       • PostgreSQL with isolated `lifeops` schema           │
│       • Row Level Security (RLS) &amp;amp; Supabase Auth            │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Frontend on Netlify ($0/mo)&lt;/strong&gt;: Single Page Application built with Vite and served globally with Gzip compression, route-level code splitting, and transparent proxy rewrite under Datalaria's domain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend on FastAPI ($0/mo)&lt;/strong&gt;: Asynchronous Python 3.13 API with strict validation via Pydantic v2, hosted on Render with cold-start resilience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database on Supabase ($0/mo)&lt;/strong&gt;: Managed PostgreSQL database with 500 MB of storage, JWT authentication, and automated Row Level Security (RLS) policies.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;[!NOTE]&lt;br&gt;
&lt;strong&gt;Render Cold-Start Handling&lt;/strong&gt;: Because the backend runs on Render.com's free tier, the container spins down after 15 minutes of inactivity. In the React frontend (detailed in Part 2), we implemented a preloader screen with automatic exponential-backoff retries, ensuring the user experiences zero abrupt connection drops.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;relational-data-modeling-clean-polymorphism-in-postgresql-&quot;&gt;Relational Data Modeling: Clean Polymorphism in PostgreSQL 🗄️&lt;/h4&gt;
&lt;p&gt;One of the biggest design challenges when combining disparate activities (a 10 km run, a 400-page book, or a Netflix movie) is avoiding monolithic tables cluttered with &lt;code&gt;NULL&lt;/code&gt; columns.&lt;/p&gt;
&lt;p&gt;To solve this, I designed a &lt;strong&gt;master table with specialized child extensions (1-to-1)&lt;/strong&gt; inside an isolated &lt;code&gt;lifeops&lt;/code&gt; schema:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 1. Master Activities Table
CREATE TABLE lifeops.activities (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    activity_type VARCHAR(30) NOT NULL, -- 'sport', 'book', 'film'
    title VARCHAR(200) NOT NULL,
    date DATE NOT NULL DEFAULT CURRENT_DATE,
    duration_minutes INTEGER,
    rating INTEGER CHECK (rating &amp;gt;= 1 AND rating &amp;lt;= 5),
    description TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 2. Fitness Extension (Workouts)
CREATE TABLE lifeops.workouts (
    activity_id UUID PRIMARY KEY REFERENCES lifeops.activities(id) ON DELETE CASCADE,
    workout_type VARCHAR(50) NOT NULL, -- 'running', 'cycling', 'gym', etc.
    distance_km NUMERIC(6,2),
    calories INTEGER,
    avg_heart_rate INTEGER,
    elevation_m INTEGER,
    personal_best BOOLEAN DEFAULT FALSE,
    notes TEXT
);

-- 3. Reading Extension (Books)
CREATE TABLE lifeops.books (
    activity_id UUID PRIMARY KEY REFERENCES lifeops.activities(id) ON DELETE CASCADE,
    author VARCHAR(150),
    pages_total INTEGER,
    pages_read INTEGER DEFAULT 0,
    status VARCHAR(30) DEFAULT 'reading', -- 'reading', 'completed', 'wishlist'
    genre VARCHAR(50)
);

-- 4. Cinema Extension (Films)
CREATE TABLE lifeops.films (
    activity_id UUID PRIMARY KEY REFERENCES lifeops.activities(id) ON DELETE CASCADE,
    media_type VARCHAR(30) DEFAULT 'movie', -- 'movie', 'series', 'documentary'
    director VARCHAR(150),
    platform VARCHAR(50), -- 'Cine', 'Netflix', 'HBO Max', 'Prime Video'
    genre VARCHAR(50),
    year INTEGER
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;why-this-architecture-shines-&quot;&gt;Why this architecture shines:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blazing Fast Global Aggregations&lt;/strong&gt;: Dashboard queries hit only &lt;code&gt;lifeops.activities&lt;/code&gt; for KPI counts and calendar activity heatmaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cascading Referencial Integrity&lt;/strong&gt;: When an activity is removed, PostgreSQL's &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; automatically purges child tables without orphaned rows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Row-Level Security (RLS)&lt;/strong&gt;: Each user can only read and mutate their own data via declarative security policies:&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE POLICY &quot;Users can only view their own activities&quot;
ON lifeops.activities FOR SELECT
USING (auth.uid() = user_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;[!IMPORTANT]&lt;br&gt;
&lt;strong&gt;Isolated Schemas &amp;amp; Permissions in Supabase&lt;/strong&gt;: When maintaining tables in a dedicated schema like &lt;code&gt;lifeops&lt;/code&gt; (rather than default &lt;code&gt;public&lt;/code&gt;), ensure you grant explicit schema access (&lt;code&gt;GRANT USAGE ON SCHEMA lifeops TO authenticated;&lt;/code&gt;) and table permissions to the &lt;code&gt;authenticated&lt;/code&gt; role. This isolates the operating system's database domain while honoring multi-tenant security.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-fastapi-backend-modular-architecture-amp-strict-typing-&quot;&gt;The FastAPI Backend: Modular Architecture &amp;amp; Strict Typing ⚡&lt;/h4&gt;
&lt;p&gt;To organize the API cleanly as business domains expand, we follow FastAPI's &lt;strong&gt;Modular Router pattern&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lifeops-api/
├── main.py                  # Entrypoint, Middlewares, Rate Limiter &amp;amp; CORS
├── config.py                # Centralized environment settings with Pydantic Settings
├── middleware/
│   └── auth.py              # Supabase JWT validation &amp;amp; secure user_id extraction
├── models/
│   ├── activity.py          # Pydantic Schemas (Create, Update, Response)
│   └── project.py           # Schemas for Tasks and Projects
├── routers/
│   ├── activities.py        # CRUD endpoints for Fitness, Books &amp;amp; Cinema
│   ├── projects.py          # Endpoints for Kanban Board and Projects
│   ├── stats.py             # Global KPI aggregations &amp;amp; Dashboard metrics
│   ├── reports.py           # Word (.docx) generator &amp;amp; CSV/Excel streaming
│   └── alerts.py            # Intelligent reminder engine
└── services/
    ├── report_generator.py  # In-memory RAM .docx compilation
    └── data_exporter.py     # CSV (BOM) &amp;amp; multi-sheet Excel generator
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;secure-supabase-jwt-authentication&quot;&gt;Secure Supabase JWT Authentication&lt;/h5&gt;
&lt;p&gt;Every endpoint processing user data injects the &lt;code&gt;get_current_user&lt;/code&gt; dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

security = HTTPBearer()

async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -&amp;gt; AuthenticatedUser:
    token = credentials.credentials
    try:
        payload = jwt.decode(
            token,
            settings.supabase_jwt_secret,
            algorithms=[&quot;HS256&quot;],
            audience=&quot;authenticated&quot;
        )
        user_id = payload.get(&quot;sub&quot;)
        if not user_id:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=&quot;Token missing user identity&quot;)
        return AuthenticatedUser(id=user_id, email=payload.get(&quot;email&quot;))
    except jwt.PyJWTError as e:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=&quot;Invalid or expired token&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;production-hardening-rate-limiting-amp-anti-abuse-&quot;&gt;Production Hardening: Rate Limiting &amp;amp; Anti-Abuse 🛡️&lt;/h4&gt;
&lt;p&gt;Because LifeOps supports on-the-fly Word report compilation and multi-sheet Excel generation, it was crucial to prevent resource starvation or DoS attacks.&lt;/p&gt;
&lt;p&gt;We integrated &lt;strong&gt;Rate Limiting with &lt;code&gt;slowapi&lt;/code&gt;&lt;/strong&gt; directly into the FastAPI application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address, default_limits=[&quot;120/minute&quot;])
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# Strict rate limit for CPU/memory-intensive Word generation
@router.post(&quot;/generate&quot;)
@limiter.limit(&quot;10/minute&quot;)
def generate_report(request: Request, req: GenerateReportRequest, user = Depends(get_current_user)):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a client exceeds the threshold, the API responds instantly with an &lt;strong&gt;HTTP 429 Too Many Requests&lt;/strong&gt;, protecting compute resources.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;conclusion-and-what-s-next-&quot;&gt;Conclusion and What's Next 🎯&lt;/h4&gt;
&lt;p&gt;With our FastAPI backend hardened, the PostgreSQL relational schema operational on Supabase, and rate limiting active, we have a production-grade foundation running at &lt;strong&gt;$0/month&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt; of this series, we dive straight into the frontend:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single Page App development with &lt;strong&gt;React 18/19, Vite, and Glassmorphism design&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internationalization (i18n)&lt;/strong&gt; system for instant Spanish 🇪🇸 / English 🇬🇧 locale toggling.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;360° Dashboard&lt;/strong&gt; architecture with aggregated KPI widgets and interactive &lt;strong&gt;Recharts&lt;/strong&gt; visualizations.&lt;/li&gt;
&lt;li&gt;Authentication context (&lt;code&gt;AuthContext&lt;/code&gt;) and session synchronization with Supabase.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;references-amp-useful-links-&quot;&gt;References &amp;amp; Useful Links 🔗&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Production Application&lt;/strong&gt;: Try the live app at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;datalaria.com/apps/lifeops&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Production REST API&lt;/strong&gt;: Interactive OpenAPI Swagger documentation at &lt;a rel=&quot;nofollow&quot; href=&quot;https://lifeops-api.onrender.com/docs&quot;&gt;lifeops-api.onrender.com/docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;FastAPI Framework&lt;/strong&gt;: Official documentation at &lt;a rel=&quot;nofollow&quot; href=&quot;https://fastapi.tiangolo.com/&quot;&gt;fastapi.tiangolo.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🗄️ &lt;strong&gt;Supabase Cloud&lt;/strong&gt;: Guide to PostgreSQL and Row Level Security at &lt;a rel=&quot;nofollow&quot; href=&quot;https://supabase.com/docs&quot;&gt;supabase.com/docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🛡️ &lt;strong&gt;SlowAPI&lt;/strong&gt;: Rate limiting for ASGI/FastAPI at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/laurentS/slowapi&quot;&gt;github.com/laurentS/slowapi&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See you in the next installment! Feel free to leave a comment below or connect on socials. 👇&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/26753/project-lifeops-part-personal-operating-system-architecture-fastapi-supabase-backend</guid>
<pubDate>Thu, 10 Sep 2026 17:06:49 +0000</pubDate>
</item>
<item>
<title>Postgres vs ClickHouse? I use both. 4 tricks from the split.</title>
<link>https://coderlegion.com/22131/postgres-vs-clickhouse-i-use-both-4-tricks-from-the-split</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; My uptime monitor keeps config and incidents in Postgres and check results in ClickHouse. The split is one rule: does a row ever change? Config gets edited, so it wants Postgres transactions and constraints. A check result is written once and never touched again, so it goes to ClickHouse, where the right codec, a careful sort key, and a per-row TTL make billions of rows cheap. Four tricks and a bonus below, useful even if you only ever run one database.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every monitor I run writes a row every 20 seconds, from every region, and never stops. One monitor is about 4,300 rows a day, per region. Multiply by every monitor on the platform and the rows only ever go up.&lt;/p&gt;
&lt;p&gt;That stream would slowly crush a normal Postgres table, and watching it is the whole product. So the check results do not live in Postgres. They live in ClickHouse. Everything else, the monitors and incidents and teams, lives in Postgres. The interesting part is the line between them.&lt;/p&gt;
&lt;p&gt;People ask why not one database. &quot;Postgres vs ClickHouse&quot; is the wrong question, because the two are not fighting over the same job. Here is the line I draw, and the one rule under all of it: split your data by how it is written, not by which engine is faster.&lt;/p&gt;
&lt;h3 id=&quot;1-does-the-row-change-that-picks-the-database&quot;&gt;1. Does the row change? That picks the database&lt;/h3&gt;
&lt;p&gt;Forget benchmarks for a second. One question sorts a table into one store or the other: after you write a row, will you ever change it?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;one monitor, two stores:

  you edit a monitor  -&amp;gt;  Postgres   (targets, incidents, team)
                          the row changes, has constraints, lives in a transaction

  a probe checks it   -&amp;gt;  ClickHouse (check_results)
                          one row, written once, never updated
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A monitor is a row that changes. You toggle it off, edit the URL, change the interval. So it lives in Postgres:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE targets (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name          TEXT NOT NULL,
    check_spec    JSONB NOT NULL,
    interval_secs INTEGER NOT NULL CHECK (interval_secs &amp;gt;= 10),
    enabled       BOOLEAN NOT NULL DEFAULT true,
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;CHECK&lt;/code&gt; means a 3-second interval can never reach the table, no matter which part of the app tried to write it. This is Postgres doing the thing it is best at: a small set of rows that must stay correct while many callers change them at once.&lt;/p&gt;
&lt;p&gt;A check result is the opposite. It is written once when a probe finishes, and then it never changes. Nothing ever updates it, so it does not need any of that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE check_results (
    org_id      UUID,
    target_id   UUID,
    region      LowCardinality(String),
    timestamp   DateTime('UTC') CODEC(DoubleDelta, ZSTD(1)),
    status      Enum8('up' = 1, 'down' = 2, 'degraded' = 3, 'error' = 4),
    duration_ms UInt32 CODEC(T64, ZSTD(1))
) ENGINE = MergeTree
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;updated_at&lt;/code&gt;, no foreign key, no transaction. Just an append-only stream that grows forever. That is the shape ClickHouse is built for and the shape that slowly hurts Postgres.&lt;/p&gt;
&lt;p&gt;Notice the types too. &lt;code&gt;status&lt;/code&gt; is an &lt;code&gt;Enum8&lt;/code&gt;, one byte on disk, not the string &lt;code&gt;&quot;up&quot;&lt;/code&gt;. &lt;code&gt;region&lt;/code&gt; is &lt;code&gt;LowCardinality&lt;/code&gt;, stored once in a dictionary and referenced by a small id instead of repeating the text on every row. On a table that only ever grows, a byte saved per row is a byte saved times billions.&lt;/p&gt;
&lt;h3 id=&quot;2-the-right-codec-turns-a-day-of-timestamps-into-almost-nothing&quot;&gt;2. The right codec turns a day of timestamps into almost nothing&lt;/h3&gt;
&lt;p&gt;Once the results are in a column store, the type on each column is most of the compression, and the default setting wastes a lot of space.&lt;/p&gt;
&lt;p&gt;Look at that timestamp again. A monitor checks every 20 seconds, so for one monitor a day of timestamps is a run of 20, 20, 20, 20, over and over. &lt;code&gt;DoubleDelta&lt;/code&gt; stores the change in the change. For a steady interval the first delta is a constant 20 and the second delta is zero, so each row after the first packs down to about a bit. The codec is not a ClickHouse invention: it comes from Facebook's Gorilla time-series paper, built for exactly this, measurements taken at a steady rate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;timestamp   DateTime('UTC')  CODEC(DoubleDelta, ZSTD(1)),
duration_ms UInt32           CODEC(T64, ZSTD(1)),
dns_ms      Nullable(UInt16) CODEC(T64, ZSTD(1)),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also store the timestamp at whole seconds on purpose. The smallest interval is 20 seconds, so no two checks for one monitor ever land in the same second, and the sub-second detail lives in &lt;code&gt;duration_ms&lt;/code&gt; where it belongs. Whole seconds that step by a fixed amount compress far harder than milliseconds would.&lt;/p&gt;
&lt;p&gt;The latency columns get &lt;code&gt;T64&lt;/code&gt; instead, because they are small integers that stay in a small range, a different shape from a steady clock. &lt;code&gt;T64&lt;/code&gt; crops the unused high bits off a block of values, so a &lt;code&gt;UInt32&lt;/code&gt; that never climbs past a few thousand milliseconds stops paying for all 32 bits. That is the trick worth copying, and it is not the specific codec names. It is that a timestamp ticking by a fixed step and a latency staying in a small range are two different shapes, and telling the database which is which does the work.&lt;/p&gt;
&lt;h3 id=&quot;3-put-the-tenant-in-the-sort-key-not-the-partition&quot;&gt;3. Put the tenant in the sort key, not the partition&lt;/h3&gt;
&lt;p&gt;I got this one wrong first, so learn it from my mistake instead of your own.&lt;/p&gt;
&lt;p&gt;This is a multi-tenant app, so every row carries an &lt;code&gt;org_id&lt;/code&gt; and almost every query filters by it. My first schema partitioned by org, one slot per customer, because it felt tidy. ClickHouse turned that into a flood of tiny parts, the merges could not keep up, and startup got slower the more customers I had. I ripped it out. A partition is not a folder for tidiness. It is a physical unit ClickHouse merges and expires, and you want few large ones, not many small ones.&lt;/p&gt;
&lt;p&gt;Here is what it should be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;) ENGINE = MergeTree
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (org_id, target_id, region, timestamp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;org_id&lt;/code&gt; leads the &lt;code&gt;ORDER BY&lt;/code&gt;, so a per-org query walks the sorted primary index and reads only that org's slice. But it stays out of &lt;code&gt;PARTITION BY&lt;/code&gt;. The rule I follow now: partition by something low-cardinality that you also delete by, here the day, and put the high-cardinality tenant key in the sort order. Sort key answers &quot;find this org fast&quot;. Partition answers &quot;drop old data cheaply&quot;, which is the next trick.&lt;/p&gt;
&lt;h3 id=&quot;4-make-retention-a-number-in-a-row-not-a-schema-change&quot;&gt;4. Make retention a number in a row, not a schema change&lt;/h3&gt;
&lt;p&gt;Different plans keep history for different lengths of time. The clumsy way is a migration or a cleanup job per plan. The clean way is to make the retention window a column:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ttl_days UInt16 DEFAULT 30 CODEC(ZSTD(1))
) ENGINE = MergeTree
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (org_id, target_id, region, timestamp)
TTL timestamp + toIntervalDay(ttl_days)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each row carries its own &lt;code&gt;ttl_days&lt;/code&gt;, stamped from the org's plan at the moment it is written. A free plan keeps 30 days, a paid plan keeps more, and changing that needs no &lt;code&gt;ALTER&lt;/code&gt;, no migration, no backfill. The next write just stores a different number. The daily partitions line up with the TTL, so ClickHouse expires old data by dropping whole parts, close to free, and a whole column of the same &lt;code&gt;30&lt;/code&gt; compresses away to nothing. The flexibility costs no space.&lt;/p&gt;
&lt;p&gt;One more piece makes reading that history cheap. A materialized view rolls raw checks into per-minute and per-hour summaries as they land:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE MATERIALIZED VIEW check_results_1m
ENGINE = AggregatingMergeTree
ORDER BY (org_id, target_id, region, minute)
AS SELECT
    org_id, target_id, region,
    toStartOfMinute(timestamp)  AS minute,
    countState()                AS total_checks,
    countIfState(status = 'up') AS up_checks,
    avgState(duration_ms)       AS avg_duration_ms
FROM check_results
GROUP BY org_id, target_id, region, minute;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So a dashboard showing 30 days across a thousand monitors reads a few thousand minute-buckets, not millions of raw rows. Recent views read the minute rollup, long history reads an hour rollup, and the raw rows underneath expire on their own TTL. The firehose is there when you need to drill into one bad minute, and left alone the rest of the time.&lt;/p&gt;
&lt;h3 id=&quot;one-more-because-everyone-hits-it-make-inserts-idempotent&quot;&gt;One more, because everyone hits it: make inserts idempotent&lt;/h3&gt;
&lt;p&gt;Here is the trick I wish someone had told me first. My agents batch check results and send them to ClickHouse. A network blip after the server commits but before my side gets the ack means the batch retries and sends the exact same block again. Without protection, that double-counts every row in it, and your uptime numbers quietly drift.&lt;/p&gt;
&lt;p&gt;ClickHouse has a fix built in, but for a plain (non-Replicated) MergeTree it is off until you turn it on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SETTINGS index_granularity = 8192,
         non_replicated_deduplication_window = 1000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server hashes each inserted block and remembers the last 1,000 hashes. A retry sends an identical block, the hash matches, and the server drops it instead of appending it again. The retry becomes safe to do blindly, so the client stays simple: send, and if unsure, send again. Make the window bigger than the most blocks a single retry could resend, and you stop having to worry about duplicate writes.&lt;/p&gt;
&lt;h3 id=&quot;so-one-database-or-two-&quot;&gt;So, one database or two?&lt;/h3&gt;
&lt;p&gt;For almost every app, one. If your data fits in Postgres and stays fast, a second database is a cost you should not pay: two schemas, two clients, two things to back up and think about.&lt;/p&gt;
&lt;p&gt;Reach for the second store only when one table stops looking like the rest of your tables. For me that table was &lt;code&gt;check_results&lt;/code&gt;. It is written once and never edited, it grows without end, and every question I ask it is a summary over a time range. That is a different shape from my monitors and my incidents, so it wanted a different database.&lt;/p&gt;
&lt;p&gt;The rule I would give my past self: do not split by &quot;which database is faster&quot;. Split by how the data is written. Rows that change and must stay correct want Postgres. An append-only stream you only ever summarize wants a column store like ClickHouse. Most of the tricks above are that one idea pushed down into the schema.&lt;/p&gt;
&lt;p&gt;Uptimepage is open source, AGPL-3.0, and both schemas are in the repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/uptimepage/uptimepage&quot;&gt;github.com/uptimepage/uptimepage&lt;/a&gt;. The probe that writes those rows is &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/http-prober-in-rust-no-reqwest&quot;&gt;its own post&lt;/a&gt;, and the wider build story, one binary and two databases, is &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/building-an-uptime-monitor-in-rust&quot;&gt;here&lt;/a&gt;. Or &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;start free on the hosted tier&lt;/a&gt; and point a check at something.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22131/postgres-vs-clickhouse-i-use-both-4-tricks-from-the-split</guid>
<pubDate>Thu, 09 Jul 2026 05:57:37 +0000</pubDate>
</item>
<item>
<title>How I Used PostgreSQL Events and MongoDB Event Logs in Two Different Real-World Systems</title>
<link>https://coderlegion.com/21940/how-i-used-postgresql-events-and-mongodb-event-logs-in-two-different-real-world-systems</link>
<description>&lt;p&gt;I built two systems that both needed event tracking.&lt;br&gt;
I used PostgreSQL in one and MongoDB in the other.&lt;br&gt;
Here's exactly why — and what I learned from doing both.&lt;br&gt;
Although the two systems were completely different in purpose, they shared one architectural requirement: each needed a reliable way to record everything that happened — not just the final state, but the story of how that state came to be.&lt;br&gt;
That sounds simple, but the nature of the events in each system was so different that using the same database for both would have caused real problems. This article explains how I approached event tracking in each system, why the solutions diverged, and what the experience taught me about choosing the right persistence model.&lt;/p&gt;
&lt;h3 id=&quot;postgresql-events-and-projections-in-the-delivery-backend&quot;&gt;PostgreSQL Events and Projections in the Delivery Backend&lt;/h3&gt;
&lt;p&gt;The delivery backend was built around structured workflows: vendors create orders, couriers pick them up, jobs move through well-defined states, and batches group multiple deliveries. Every action in this system has a clear relational meaning. An event like ORDER_CREATED or JOB_STARTED isn't just a log entry — it's part of the domain model.&lt;br&gt;
Because of that, PostgreSQL was the natural home for events. Each event needed to be tied to a specific entity, validated against constraints, indexed for fast lookup, and projected into timeline tables that the dashboard could query efficiently.&lt;br&gt;
The event log table looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;model EventLog {
  id          Int             @id @default(autoincrement())
  entityType  EventEntityType
  entityId    Int
  eventType   String
  payload     Json
  createdAt   DateTime        @default(now())

  @@index([entityType, entityId, createdAt])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every event is immutable. Every event belongs to a relational entity. Every event carries structured metadata.&lt;br&gt;
Once an event is written, it is immediately transformed into a timeline entry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;switch (event.entityType) {
  case EventEntityType.ORDER:
    await this.orderTimelineProjection.project(event);
    break;

  case EventEntityType.JOB:
    await this.jobTimelineProjection.project(event);
    break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This projection layer is what makes the system feel fast. Instead of running complex joins to reconstruct history, the dashboard reads from dedicated timeline tables optimized for sequential queries. It also makes debugging easier: if something goes wrong, I can replay the event stream and see exactly how the system reached its current state.&lt;br&gt;
PostgreSQL excels in this environment because the events are structured, relational, and part of the domain's consistency model. They aren't just analytics — they're part of the truth.&lt;/p&gt;
&lt;h3 id=&quot;mongodb-event-logs-and-aggregation-in-waitless&quot;&gt;MongoDB Event Logs and Aggregation in Waitless&lt;/h3&gt;
&lt;p&gt;Waitless is a completely different kind of system. It's a real-time queue management platform where customers join via QR code, track their position live, and leave without creating an account. The system generates a lot of events — but these events are not relational. They're behavioral.&lt;br&gt;
A CUSTOMER_JOINED event might contain the customer's position.&lt;br&gt;
A NO_SHOW event might contain a timestamp.&lt;br&gt;
A CUSTOMER_LEFT event might contain a reason.&lt;br&gt;
A CALLED_NEXT event might contain which token was called.&lt;/p&gt;
&lt;p&gt;The shape of the metadata changes depending on the event type. Forcing this into a rigid SQL schema would either require dozens of nullable columns or a JSON column that PostgreSQL would struggle to index efficiently at scale.&lt;br&gt;
MongoDB is built for exactly this. Each event is a document with flexible metadata:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Schema({ timestamps: true })
export class QueueEvent {
  @Prop({ required: true })
  queueId!: string;

  @Prop({ required: true, enum: QueueEventType })
  eventType!: QueueEventType;

  @Prop()
  token?: string;

  @Prop({ type: Object })
  metadata?: Record&amp;lt;string, any&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This flexibility becomes powerful when combined with MongoDB's aggregation pipeline. Waitless computes analytics like total joined, total arrived, total no-shows, completion rate, and drop-off rate — all derived from event counts, not relational queries.&lt;br&gt;
A note on Redis: Waitless already had Redis in the stack backing BullMQ. But Redis is a key-value store optimized for job queues and caching — not for querying across documents or running aggregations. MongoDB was the right fit for this specific need.&lt;br&gt;
The separation between MongoDB and PostgreSQL is also crucial for performance. If analytics were computed in PostgreSQL, every dashboard refresh would compete with real-time queue operations for the same connection pool.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If I had used PostgreSQL for Waitless events, every analytics query would hit the same connection pool as the queue state — meaning a slow dashboard query could delay a customer joining a queue.&lt;br&gt;
MongoDB avoids this entirely. It handles flexible event shapes, high write throughput, and analytics workloads without interfering with the transactional state stored in PostgreSQL.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;why-postgresql-for-one-and-mongodb-for-the-other-&quot;&gt;Why PostgreSQL for One and MongoDB for the Other?&lt;/h3&gt;
&lt;p&gt;The answer becomes clear once you see the nature of the data.&lt;br&gt;
In the delivery backend, events are part of the domain model. They describe state transitions, belong to relational entities, and require strong consistency. PostgreSQL is the right tool.&lt;br&gt;
In Waitless, events are part of analytics. They are flexible, metadata-heavy, and high-volume. They have no relational meaning — they're behavioral observations about what happened in a queue session. MongoDB is the right tool.&lt;br&gt;
Using PostgreSQL for both would have created unnecessary coupling and performance bottlenecks. Using MongoDB for both would have sacrificed relational guarantees where they mattered most.&lt;br&gt;
This is the essence of polyglot persistence: choose the database that matches the shape and purpose of the data, not the one you're most comfortable with.&lt;/p&gt;
&lt;h3 id=&quot;the-question-that-actually-matters&quot;&gt;The Question That Actually Matters&lt;/h3&gt;
&lt;p&gt;Building two event-driven systems taught me that &quot;SQL vs NoSQL&quot; is the wrong question entirely.&lt;br&gt;
The real question is: what role do your events play in the system?&lt;br&gt;
If events belong to the domain model — use a relational database.&lt;br&gt;
If events belong to analytics — use a document database.&lt;br&gt;
PostgreSQL gave me structured timelines and strong consistency. MongoDB gave me flexible event logs and fast aggregation. Both systems use events, but the events serve completely different masters.&lt;br&gt;
Understanding that difference before writing a single line of code is what separates a working system from a well-designed one.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21940/how-i-used-postgresql-events-and-mongodb-event-logs-in-two-different-real-world-systems</guid>
<pubDate>Sun, 05 Jul 2026 09:20:40 +0000</pubDate>
</item>
<item>
<title>Working with nested JSON: exploring it, mocking it, and querying it in Postgres</title>
<link>https://coderlegion.com/21844/working-with-nested-json-exploring-it-mocking-it-and-querying-it-in-postgres</link>
<description>&lt;p&gt;Flat JSON is easy. Nested JSON is where the day goes: an API response with orders inside customers inside accounts, a jsonb column three levels deep, a config file that grew a tree. Three tasks come up over and over, and each one has a trick that makes it painless. Here they are.&lt;/p&gt;
&lt;h3 id=&quot;1-reading-a-huge-nested-payload&quot;&gt;1. Reading a huge nested payload&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5919876841020931874&quot; alt=&quot;Tree View&quot;&gt;&lt;/p&gt;
&lt;p&gt;Pretty-printing a 4,000-line response does not actually help. The indentation is correct, but finding one thing still means scrolling through thousands of lines. The fix is to stop treating JSON as text to scroll and start treating it as data to query.&lt;/p&gt;
&lt;p&gt;Say the question is: does any item in this order have a null price?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;account&quot;: {
    &quot;id&quot;: 91,
    &quot;customer&quot;: {
      &quot;orders&quot;: [
        { &quot;id&quot;: 4821, &quot;items&quot;: [ { &quot;sku&quot;: &quot;A-19&quot;, &quot;price&quot;: 12.5 } ] },
        { &quot;id&quot;: 4822, &quot;items&quot;: [ { &quot;sku&quot;: &quot;B-04&quot;, &quot;price&quot;: null } ] }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three moves turn this from a scroll into a lookup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter by path, key, type, or value. Filtering to price pulls every price field out of the whole tree at once. Filtering by the value null jumps straight to the offending row. The needle in 4,000 lines becomes a single filtered result.&lt;/li&gt;
&lt;li&gt;Select the nesting level. Instead of expanding everything, expand the tree to level 2 to see the shape of the data, then drill only into the branch that matters. This is the difference between reading structure and drowning in leaves.&lt;/li&gt;
&lt;li&gt;Switch to a table view when a branch is really a list of records. Two hundred objects with the same keys is a spreadsheet pretending to be JSON, and reading it as rows and columns is far faster than reading it as nested braces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once JSON is filterable and foldable, payload size stops mattering.&lt;/p&gt;
&lt;h3 id=&quot;2-generating-realistic-nested-json-fast&quot;&gt;2. Generating realistic nested JSON, fast&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4516247635221435725&quot; alt=&quot;Generate Mock JSON&quot;&gt;&lt;/p&gt;
&lt;p&gt;The opposite problem: there is no payload yet, and building a parser, a UI, or a test needs one. Hand-writing mock data is slow, and worse, it is too clean. Every record ends up with the same id, the same date, the same shape, which is exactly the uniformity that hides off-by-one bugs, dedupe bugs, and sort bugs.&lt;/p&gt;
&lt;p&gt;Two shortcuts cover most cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load a realistic nested sample. Start from a ready-made nested document (an order, a log line, an analytics event) and adapt it, instead of inventing structure from scratch.&lt;/li&gt;
&lt;li&gt;Multiply one object into many varied records. Write a single object by hand, pick a count, and get back an array where fields like id, email, and dates actually vary per record. That variation is the point: a list of 50 rows that are genuinely different is what surfaces the bug that 50 identical rows would hide.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Realistic nested test data in seconds beats a hand-typed array of clones every time.&lt;/p&gt;
&lt;h3 id=&quot;3-querying-and-updating-nested-json-in-postgres&quot;&gt;3. Querying and updating nested JSON in Postgres&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14815475663515361781&quot; alt=&quot;Querying in PostgreSQL&quot;&gt;&lt;/p&gt;
&lt;p&gt;Eventually that nested JSON lands in a jsonb column, and a viewer cannot help anymore. This is where people get stuck, so here is the practical core.&lt;/p&gt;
&lt;p&gt;Assume a table orders with a jsonb column data holding the payload above.&lt;/p&gt;
&lt;p&gt;Selecting. The two operators to internalize are &lt;code&gt;-&amp;gt;&lt;/code&gt; and &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;. &lt;code&gt;-&amp;gt;&lt;/code&gt; returns JSON, &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; returns text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- returns jsonb
SELECT data -&amp;gt; 'account' -&amp;gt; 'customer' FROM orders;

-- returns text (note the final -&amp;gt;&amp;gt;)
SELECT data -&amp;gt; 'account' -&amp;gt;&amp;gt; 'id' AS account_id FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a deep path, &lt;code&gt;#&amp;gt;&lt;/code&gt; and &lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; take an array of keys and are much cleaner than chaining arrows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- text value deep in the structure
SELECT data #&amp;gt;&amp;gt; '{account,customer,orders,0,id}' AS first_order_id
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To filter rows by a nested value, containment (&lt;code&gt;@&amp;gt;&lt;/code&gt;) is both readable and indexable with a GIN index:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM orders
WHERE data @&amp;gt; '{&quot;account&quot;: {&quot;id&quot;: 91}}';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And to work across an array, expand it into rows with &lt;code&gt;jsonb_array_elements&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT elem -&amp;gt;&amp;gt; 'id' AS order_id
FROM orders,
     jsonb_array_elements(data #&amp;gt; '{account,customer,orders}') AS elem;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Updating. The workhorse is &lt;code&gt;jsonb_set(target, path, new_value)&lt;/code&gt;. The gotcha that trips everyone: &lt;code&gt;new_value&lt;/code&gt; must itself be &lt;code&gt;jsonb&lt;/code&gt;, so cast it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- set a deep scalar (note the ::jsonb cast)
UPDATE orders
SET data = jsonb_set(data, '{account,customer,orders,0,id}', '9001'::jsonb)
WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add or replace a key at a level with the || merge operator, and remove a path with &lt;code&gt;#-&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- shallow-merge a new key into the top level
UPDATE orders SET data = data || '{&quot;archived&quot;: true}'::jsonb WHERE id = 1;

-- delete a nested key
UPDATE orders SET data = data #- '{account,customer,orders,0,items}' WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One honest warning: &lt;code&gt;||&lt;/code&gt; merges only one level deep, so it will not deep-merge nested objects. For nested changes, reach for &lt;code&gt;jsonb_set&lt;/code&gt;. And the genuinely hard case, updating a specific array element chosen by a condition rather than a fixed index, needs more than &lt;code&gt;jsonb_set&lt;/code&gt; alone. That is a whole topic on its own, and it is the one worth reading a dedicated write-up on before you fight it in production.&lt;/p&gt;
&lt;p&gt;Here is the honest reason all of this matters: nobody keeps these operators in their head. It is &lt;code&gt;-&amp;gt;&lt;/code&gt; versus &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;, the &lt;code&gt;#&amp;gt;&amp;gt;&lt;/code&gt; path syntax, the &lt;code&gt;::jsonb&lt;/code&gt; cast that &lt;code&gt;jsonb_set&lt;/code&gt; quietly requires, the fact that &lt;code&gt;||&lt;/code&gt; only merges one level. I forget them between projects every single time. So I wrote them down as a permanent, on-page reference next to the formatter: &lt;a rel=&quot;nofollow&quot; href=&quot;https://catssaymeow.org/json-formatter/#querying-json-in-postgresql&quot;&gt;Querying nested JSON in PostgreSQL (SELECT and UPDATE)&lt;/a&gt; collects the common reads and writes in one place, for the moment memory does not serve. It is reference for the everyday cases, not a full Postgres manual.&lt;/p&gt;
&lt;h3 id=&quot;where-the-tool-fits&quot;&gt;Where the tool fits&lt;/h3&gt;
&lt;p&gt;I built all three of these into one place, at &lt;a rel=&quot;nofollow&quot; href=&quot;https://catssaymeow.org/json-formatter/&quot;&gt;catssaymeow.org/json-formatter&lt;/a&gt;, because I got tired of one tab that only pretty-prints and another that only explores.&lt;/p&gt;
&lt;p&gt;It formats, validates, and minifies (invalid JSON is pinpointed by line and column, not a bare &quot;invalid&quot;), and then lets you actually work with the structure: explore it as a tree, graph, or table, filter by path, key, type, or value, and expand to a chosen nesting level. When there is no data to work with yet, it can load a realistic nested sample or multiply one object into an array of varied mock records. It runs as client-side JavaScript, so the JSON stays in the browser, and clean JSON can be sent straight to a built-in API Client as a request body.&lt;/p&gt;
&lt;p&gt;And for the part that happens after the database call, the same page carries a &lt;a rel=&quot;nofollow&quot; href=&quot;https://catssaymeow.org/json-formatter/#querying-json-in-postgresql&quot;&gt;Postgres jsonb querying reference&lt;/a&gt; for the SELECT and UPDATE syntax that never sticks. Three recurring nested-JSON tasks, one page to come back to.&lt;/p&gt;
&lt;p&gt;If you put it up against your own worst nested payload, I would like to hear what it struggles with.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21844/working-with-nested-json-exploring-it-mocking-it-and-querying-it-in-postgres</guid>
<pubDate>Fri, 03 Jul 2026 13:52:48 +0000</pubDate>
</item>
<item>
<title>Your Database GUI Shouldn't Need an Account</title>
<link>https://coderlegion.com/20175/your-database-gui-shouldnt-need-an-account</link>
<description>&lt;p&gt;A critique of the trend toward account-required database GUI clients, arguing that tools holding production credentials should never route data through vendor backends. The post makes the case for local-first design using Tabularis as a concrete example: no signup, secrets stored in the OS keychain, all config as plain files on disk, bring-your-own-key AI, and only one network call (a GitHub version check). The tradeoffs — no sync, no team workspaces, no telemetry — are acknowledged openly. The core argument is that database clients are plumbing and should be durable and vendor-independent, not coupled to a company's runway.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20175/your-database-gui-shouldnt-need-an-account</guid>
<pubDate>Wed, 10 Jun 2026 13:52:44 +0000</pubDate>
</item>
<item>
<title>Building an AI Product Backend From Scratch: FastAPI, Postgres, pgvector, Stripe, and What I'd Do Di</title>
<link>https://coderlegion.com/18181/building-an-ai-product-backend-from-scratch-fastapi-postgres-pgvector-stripe-and-what-id</link>
<description>&lt;p&gt;A couple of years ago I was hired as the solo backend engineer to build the production backend for an AI product company from scratch. No legacy code, no existing schema, no team to defer to — just a frontend team waiting on an API and a product team waiting on inference. This post is about the architecture decisions I made, what worked, and what I'd do differently if I were starting today.&lt;br&gt;
The shape of the problem&lt;br&gt;
The product needed five things working together:&lt;/p&gt;
&lt;p&gt;A typed API for the frontend&lt;br&gt;
LLM inference with reasonable UX (streaming, cost controls, retries)&lt;br&gt;
Vector search over user-supplied content&lt;br&gt;
Authentication and user management&lt;br&gt;
Subscription billing&lt;/p&gt;
&lt;p&gt;Standard stuff individually. The interesting part was keeping the operational footprint small enough that one person could run the whole thing without it falling over.&lt;br&gt;
Why FastAPI + Postgres + pgvector&lt;br&gt;
The temptation when building an AI product is to reach for a Lambda-and-microservices architecture from day one. I went the other direction: one FastAPI service, one Postgres database, pgvector for embeddings inside that same Postgres.&lt;br&gt;
The reasoning:&lt;/p&gt;
&lt;p&gt;One database, one source of truth. User data, subscription state, and vector embeddings live in the same Postgres. No sync issues between Postgres and a separate vector DB. Backups are one operation. Migrations are one tool (Alembic).&lt;br&gt;
FastAPI's typing pays for itself. Pydantic schemas become the contract with the frontend. OpenAPI generation is free. Async support means LLM calls and external APIs don't block the event loop.&lt;br&gt;
One service is faster to ship and easier to debug. When something breaks at 2am, you don't have to figure out which of seven services owns the bug.&lt;/p&gt;
&lt;p&gt;The cost: at very high scale (millions of users, billions of embeddings), pgvector and a monolithic FastAPI service would need rethinking. For an early-stage product, this was the right tradeoff.&lt;br&gt;
LLM integration: the part people underestimate&lt;br&gt;
Connecting to an LLM API is one line of code. Making it production-grade is most of the engineering work. The pieces:&lt;/p&gt;
&lt;p&gt;Streaming responses. Users will not wait 15 seconds for a wall of text. Server-sent events from FastAPI through to the frontend, with proper backpressure handling.&lt;br&gt;
Token accounting per user. Every request logs prompt tokens, completion tokens, and cost. Without this you have no idea who's burning your budget.&lt;br&gt;
Soft caps + fallback models. Hitting a per-user daily limit doesn't return an error — it switches to a cheaper model and continues. Users get a worse experience, not a broken one.&lt;br&gt;
Retry with backoff that respects rate limits. Upstream providers throttle. Naive retry loops make it worse. Exponential backoff plus reading the Retry-After header is the minimum.&lt;br&gt;
Prompt templates separated from application code. Templates live in their own module, versioned, with the application code only choosing which one to invoke. Changes to wording don't require touching business logic.&lt;/p&gt;
&lt;p&gt;Vector search inside Postgres&lt;br&gt;
pgvector is one of the more underrated decisions you can make. It's a Postgres extension that adds a vector type and similarity-search operators. You get nearest-neighbor queries with the same SQL you use for everything else, the same transaction guarantees, and the same backup story.&lt;br&gt;
The gotchas worth knowing:&lt;/p&gt;
&lt;p&gt;Indexes matter a lot. A naive sequential scan over a few hundred thousand vectors will be slow. IVFFlat or HNSW indexes are not optional past a certain table size.&lt;br&gt;
Chunking is where retrieval quality lives. Embedding a 50-page document as one vector is useless. Chunk strategy (size, overlap, semantic boundaries) affects results more than the embedding model choice.&lt;br&gt;
Re-embedding is expensive. When you change embedding models or chunking strategy, you re-embed everything. Design the ingestion pipeline so this isn't catastrophic.&lt;/p&gt;
&lt;p&gt;Subscription billing: webhooks are the product&lt;br&gt;
Stripe handles the payment UI and the regulatory complexity. Your job is to receive its webhooks and turn them into entitlements in your database.&lt;br&gt;
The non-negotiables:&lt;/p&gt;
&lt;p&gt;Webhooks must be idempotent. Stripe will deliver the same event twice. If your subscription state changes twice, you've granted access twice or revoked it incorrectly.&lt;br&gt;
Webhook signature verification on every request. Without it, anyone can POST a fake &quot;subscription.created&quot; event to your endpoint.&lt;br&gt;
Entitlements as a separate layer. Don't check Stripe's subscription status in every API handler. Maintain a local entitlements table updated by webhooks; check that. It's faster, more reliable, and survives Stripe outages.&lt;br&gt;
Reconciliation jobs. Webhooks fail. A nightly job that pulls the truth from Stripe and reconciles your local state catches the gaps before users notice.&lt;/p&gt;
&lt;p&gt;Auth: boring is correct&lt;br&gt;
Email/password with hashed credentials, OAuth for social sign-in, JWT for sessions with refresh tokens, role-based access control on routes. None of this is novel and none of it should be. The mistake people make on AI products is wanting auth to be clever. It shouldn't be. Pick well-tested libraries and move on.&lt;br&gt;
What I'd do differently today&lt;br&gt;
A few things have changed in the 2-3 years since:&lt;/p&gt;
&lt;p&gt;Structured outputs. Most LLM providers now support enforced JSON schema responses. When I built this, you parsed JSON out of completions and prayed. Today I'd use structured outputs everywhere they're supported and delete a lot of validation code.&lt;br&gt;
Streaming standards. The streaming UX patterns are much more mature now. Vercel AI SDK and similar libraries handle a lot of what I wrote by hand.&lt;br&gt;
Embedding model choices. The embedding landscape moved fast. I'd benchmark a few current options against the actual retrieval task instead of defaulting to OpenAI embeddings.&lt;br&gt;
Observability from day one. I added logging and error reporting, but I'd build proper LLM-aware observability (token usage, latency percentiles, prompt versions, retrieval quality) into the request path from the first commit.&lt;/p&gt;
&lt;p&gt;Takeaways&lt;br&gt;
If you're building an AI product backend:&lt;/p&gt;
&lt;p&gt;One service and one database will take you further than you think&lt;br&gt;
Most of the engineering work is around the LLM call, not the LLM call itself&lt;br&gt;
pgvector is a serious option if you're already on Postgres&lt;br&gt;
Webhook idempotency and entitlement layers will save you from a class of bugs that are hard to debug after the fact&lt;/p&gt;
&lt;p&gt;Happy to dig into any of these in the comments — especially if you've made different architectural choices and have war stories to share.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18181/building-an-ai-product-backend-from-scratch-fastapi-postgres-pgvector-stripe-and-what-id</guid>
<pubDate>Mon, 18 May 2026 02:16:31 +0000</pubDate>
</item>
<item>
<title>MCP-PostgreSQL-Ops</title>
<link>https://coderlegion.com/16311/mcp-postgresql-ops</link>
<description>&lt;h3 id=&quot;what-is-mcp-postgresql-ops-&quot;&gt;What is MCP-PostgreSQL-Ops?&lt;/h3&gt;
&lt;p&gt;A &lt;strong&gt;production-ready Model Context Protocol (MCP) server&lt;/strong&gt; for PostgreSQL &lt;br&gt;
operations and monitoring. 30+ tools that let you analyze, monitor, and &lt;br&gt;
maintain PostgreSQL databases through natural language — works with &lt;br&gt;
Claude Desktop, Claude Code, OpenWebUI, or any MCP-compatible client.&lt;/p&gt;
&lt;p&gt;Stop juggling psql, pgAdmin, and custom monitoring scripts. Just ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Show me the slowest queries in the last hour&quot;&lt;/li&gt;
&lt;li&gt;&quot;Which tables have high bloat and need VACUUM?&quot;&lt;/li&gt;
&lt;li&gt;&quot;Analyze autovacuum activity for the past 48 hours&quot;&lt;/li&gt;
&lt;li&gt;&quot;Show all foreign key relationships for the customers table&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-i-built-it&quot;&gt;Why I built it&lt;/h3&gt;
&lt;p&gt;Most PostgreSQL MCP servers are thin SQL wrappers. This one is built for &lt;br&gt;
real DBA workflows — performance diagnosis, bloat analysis, autovacuum &lt;br&gt;
monitoring, schema introspection — without requiring superuser access or &lt;br&gt;
exotic extensions.&lt;/p&gt;
&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;30+ tools&lt;/strong&gt; covering connections, configs, schemas, indexes, IO, &lt;br&gt;
WAL, replication, locks, and more&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL 12–17 support&lt;/strong&gt; with automatic version detection and &lt;br&gt;
graceful feature degradation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read-only and production-safe&lt;/strong&gt; — works with regular role permissions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RDS / Aurora compatible&lt;/strong&gt; — managed service friendly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extension-independent core&lt;/strong&gt; — most tools work without &lt;br&gt;
pg_stat_statements; advanced query analytics light up automatically &lt;br&gt;
when extensions are installed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-database operations&lt;/strong&gt; — switch between databases via parameter, &lt;br&gt;
no reconnection needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloat &amp;amp; autovacuum intelligence&lt;/strong&gt; — dead tuple ratios, VACUUM &lt;br&gt;
effectiveness analysis, autovacuum trigger thresholds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bearer token auth&lt;/strong&gt; for streamable-http mode in production&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;PyPI package available:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install mcp-postgresql-ops
# or
uvx --python 3.12 mcp-postgresql-ops
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude Desktop config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;mcp-postgresql-ops&quot;: {
      &quot;command&quot;: &quot;uvx&quot;,
      &quot;args&quot;: [&quot;--python&quot;, &quot;3.12&quot;, &quot;mcp-postgresql-ops&quot;],
      &quot;env&quot;: {
        &quot;POSTGRES_HOST&quot;: &quot;127.0.0.1&quot;,
        &quot;POSTGRES_PORT&quot;: &quot;5432&quot;,
        &quot;POSTGRES_USER&quot;: &quot;postgres&quot;,
        &quot;POSTGRES_PASSWORD&quot;: &quot;your-password&quot;,
        &quot;POSTGRES_DB&quot;: &quot;your-db&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or run the full Docker Compose stack with built-in test data, OpenWebUI, &lt;br&gt;
and MCPO proxy in 5 minutes.&lt;/p&gt;
&lt;h3 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.12 + asyncpg&lt;/li&gt;
&lt;li&gt;FastMCP framework&lt;/li&gt;
&lt;li&gt;stdio and streamable-http transports&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;looking-for-feedback&quot;&gt;Looking for Feedback&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Which additional PostgreSQL areas should I cover next? (Logical &lt;br&gt;
replication slots? Partitioning analysis? Extension-specific tools?)&lt;/li&gt;
&lt;li&gt;Anyone running this against large production clusters — performance &lt;br&gt;
feedback welcome&lt;/li&gt;
&lt;li&gt;Cross-version testing reports (especially PG 12 and PG 17 edge cases)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Repo&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/call518/MCP-PostgreSQL-Ops&quot;&gt;https://github.com/call518/MCP-PostgreSQL-Ops&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/MCP-PostgreSQL-Ops/&quot;&gt;https://pypi.org/project/MCP-PostgreSQL-Ops/&lt;/a&gt;  &lt;/p&gt;
&lt;p&gt;⭐ Stars, issues, and PRs welcome. The codebase is structured for easy &lt;br&gt;
custom-tool addition — see the &quot;Adding Custom Tools&quot; section in the README.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/16311/mcp-postgresql-ops</guid>
<pubDate>Fri, 01 May 2026 08:46:46 +0000</pubDate>
</item>
<item>
<title>The Risk Map: Architecting an Obsolescence-Immune Data Foundation</title>
<link>https://coderlegion.com/15801/the-risk-map-architecting-an-obsolescence-immune-data-foundation</link>
<description>&lt;h3 id=&quot;1-the-flat-table-trap-excel-is-dead-&quot;&gt;1. The Flat Table Trap (Excel is Dead)&lt;/h3&gt;
&lt;p&gt;Managing an industrial &lt;em&gt;Bill of Materials&lt;/em&gt; (BOM) through spreadsheets is a structural deficiency. Excel provides a two-dimensional environment for a three-dimensional problem. Hardware engineering and manufacturing dependencies operate under a graph logic.&lt;/p&gt;
&lt;p&gt;When the procurement department receives an &lt;em&gt;End of Life&lt;/em&gt; (EOL) alert for a component, calculating the volumetric impact by searching text across multiple static documents introduces operational latency.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;UNE-EN IEC 62402:2019&lt;/strong&gt; standard, in its &lt;strong&gt;Clause 8.10 (Data Acquisition)&lt;/strong&gt;, establishes the requirement to maintain &quot;a list of configuration sub items within an item&quot; alongside &quot;the identification of the items and sub items details: manufacturer, part number and specification&quot;. Achieving the level of parametric traceability demanded by the standard requires the design of a relational data model.&lt;/p&gt;
&lt;h3 id=&quot;2-modeling-reality-the-graph-approach-the-internal-environment-&quot;&gt;2. Modeling Reality: The Graph Approach (The Internal Environment)&lt;/h3&gt;
&lt;p&gt;A bill of materials is a dependency tree. To structure the internal manufacturing environment data in PostgreSQL, we define three main indexed tables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;products&lt;/code&gt;&lt;/strong&gt;: Represents the end products. Includes the &lt;code&gt;gross_margin&lt;/code&gt; parameter, required to correlate component supply risk with the financial impact on the P&amp;amp;L.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;internal_parts&lt;/code&gt;&lt;/strong&gt;: A consolidated register of components, raw materials, and sub-assemblies defined by engineering. Uses a global unique identifier (&lt;code&gt;internal_pn&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bom_lines&lt;/code&gt;&lt;/strong&gt;: Implements the hierarchical relationship. Defines dependencies through mutually exclusive foreign keys (&lt;code&gt;parent_product_id&lt;/code&gt; or &lt;code&gt;parent_assembly_id&lt;/code&gt;) to a child component (&lt;code&gt;child_pn&lt;/code&gt;), specifying a &lt;code&gt;quantity&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Technical Validation:&lt;/strong&gt; Normalizing the data into a graph structure enables the use of recursive algorithms capable of calculating total exposure in milliseconds. If &quot;Equipment A&quot; requires 4 capacitors and &quot;Equipment B&quot; requires 12, a shortage alert on the component node propagates mathematically and automatically up the tree, cross-referencing totals against the order backlog (SLAs).&lt;/p&gt;
&lt;h3 id=&quot;3-the-firewall-the-aml-table-approved-manufacturer-list-&quot;&gt;3. The Firewall: The AML Table (Approved Manufacturer List)&lt;/h3&gt;
&lt;p&gt;A core principle of industrial inventory integrity is to prevent the mixing of internal nomenclature with dynamic global market references. Omitting this separation generates traceability inconsistencies.&lt;/p&gt;
&lt;p&gt;The architecture implements the &lt;strong&gt;&lt;code&gt;aml&lt;/code&gt; (Approved Manufacturer List)&lt;/strong&gt; table as an interface between internal hardware design and the supply chain.&lt;/p&gt;
&lt;p&gt;This table enables the &lt;strong&gt;Dual-Sourcing&lt;/strong&gt; strategy. If the bill of materials specifies the component &lt;code&gt;CAP-10K&lt;/code&gt; (ERP code), the &lt;code&gt;aml&lt;/code&gt; table relates this internal node to both a Texas Instruments reference (&lt;code&gt;preference_level: Primary&lt;/code&gt;) and a validated equivalent from Analog Devices (&lt;code&gt;preference_level: Secondary&lt;/code&gt;). Upon a supply interruption from the primary source, the relational logic allows switching demand to the secondary source without altering the base BOM.&lt;/p&gt;
&lt;h3 id=&quot;4-the-integrated-radar-the-external-environment-&quot;&gt;4. The Integrated Radar (The External Environment)&lt;/h3&gt;
&lt;p&gt;Integration with global commercial dynamics is consolidated in the &lt;strong&gt;&lt;code&gt;manufacturer_parts&lt;/code&gt;&lt;/strong&gt; table.&lt;/p&gt;
&lt;p&gt;This entity receives telemetry flows from market aggregators (&lt;em&gt;SiliconExpert&lt;/em&gt;, &lt;em&gt;IHS Markit&lt;/em&gt;, &lt;em&gt;Accuris&lt;/em&gt;) through asynchronous integrations. It maintains an updated record of the manufacturer part numbers (&lt;code&gt;mpn&lt;/code&gt;), their lifecycle state (&lt;code&gt;lifecycle_status&lt;/code&gt;), and the estimated obsolescence date (&lt;code&gt;estimated_eol_date&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/obs_part3_architecture/architecture.png&quot; alt=&quot;architecture&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;5-data-segmentation-and-intellectual-property-rls-in-supabase-&quot;&gt;5. Data Segmentation and Intellectual Property (RLS in Supabase)&lt;/h3&gt;
&lt;p&gt;Deploying this model involves managing Intellectual Property (IP) assets. Accidental or unauthorized exposure of the complete product topology is an unacceptable information security risk. Leveraging the Supabase platform allows implementing security architectures at the data layer.&lt;/p&gt;
&lt;p&gt;To prevent external generative models (LLMs) or unprivileged users from reconstructing the complete BOM dependencies, we apply &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt; directly in PostgreSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Security: Row Level Security (RLS) policies - IP Protection
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
ALTER TABLE bom_lines ENABLE ROW LEVEL SECURITY;

-- Allow general read access to authenticated logic
CREATE POLICY &quot;Allow read access to authenticated users&quot; 
ON products FOR SELECT TO authenticated USING (true);

-- Restrict mutations (insert/update/delete) on products and BOM lines to Admin role exclusively
CREATE POLICY &quot;Restrict products mutations to admins&quot; 
ON products FOR ALL TO authenticated 
USING ((auth.jwt() -&amp;gt;&amp;gt; 'role') = 'admin');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The policies restrict any modifications to the &lt;code&gt;products&lt;/code&gt; and &lt;code&gt;bom_lines&lt;/code&gt; tables to the &quot;Admin&quot; role. Additionally, the architectural design dictates that the database must parse the mathematical expansion of the tree internally and provide the external AI Agent with only the aggregated risk value, without returning the topological structure.&lt;/p&gt;
&lt;h3 id=&quot;6-next-steps&quot;&gt;6. Next Steps&lt;/h3&gt;
&lt;p&gt;The relational database schema for obsolescence management is defined based on modular integrity principles.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The next step in the data architecture process will focus on integration automation. We will evaluate development in Python, using the Pandas library, to engineer an extraction, transformation, and loading (ETL) pipeline capable of ingesting BOM lists in Excel format and structurally populating them into this Supabase ecosystem. Subscribe to access this operational phase.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15801/the-risk-map-architecting-an-obsolescence-immune-data-foundation</guid>
<pubDate>Sat, 25 Apr 2026 07:47:17 +0000</pubDate>
</item>
<item>
<title>Database Optimization Strategies That Cut Query Execution Time by 60%: A Practical Guide</title>
<link>https://coderlegion.com/14921/database-optimization-strategies-that-cut-query-execution-time-by-60-a-practical-guide</link>
<description>&lt;p&gt;If your web application is slow, the database is almost certainly the bottleneck. Not the frontend framework. Not the web server. Not the network. The database.&lt;/p&gt;
&lt;p&gt;I have spent nine years building backend systems across seven companies, and in every single engagement, database performance has been the most impactful area of optimization. At VacancySoft, where I currently serve as a Full Stack Engineer, I reduced query execution times by 60% across our core platform. At Univelcity, I achieved a 45% reduction in API response times, with database optimization accounting for the majority of those gains. At Lordwin Group, I optimised a real-time gaming backend to sustain 3,000+ concurrent users with sub-100ms query responses.&lt;/p&gt;
&lt;p&gt;This article is the practical guide I wish I had when I started. It covers the exact strategies, tools, and techniques I use to diagnose and fix database performance problems in production systems running PostgreSQL and MySQL.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;strategy-1-query-analysis-and-profiling-know-before-you-optimize&quot;&gt;Strategy 1: Query Analysis and Profiling — Know Before You Optimize&lt;/h3&gt;
&lt;p&gt;The biggest mistake engineers make with database optimization is guessing. They add indexes randomly, rewrite queries based on intuition, or throw caching at the problem without understanding what is actually slow.&lt;/p&gt;
&lt;p&gt;I start every optimization effort the same way: &lt;strong&gt;profiling&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;enable-slow-query-logging&quot;&gt;Enable Slow Query Logging&lt;/h4&gt;
&lt;p&gt;In PostgreSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- postgresql.conf
log_min_duration_statement = 200  -- Log queries taking &amp;gt; 200ms
log_statement = 'none'            -- Don't log all queries, just slow ones
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In MySQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.2;  -- 200ms threshold
SET GLOBAL log_queries_not_using_indexes = 'ON';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At VacancySoft, I established a weekly ritual of reviewing the slow query log. This alone surfaced dozens of optimization opportunities that no amount of code review would have caught. Queries that looked clean in the ORM were generating catastrophic execution plans.&lt;/p&gt;
&lt;h4 id=&quot;use-explain-analyze-religiously&quot;&gt;Use EXPLAIN ANALYZE Religiously&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt; shows you the query plan. &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; actually executes the query and shows real timing data. The difference matters.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN ANALYZE
SELECT v.id, v.title, o.name AS organisation_name, s.name AS sector_name
FROM vacancies v
JOIN organisations o ON o.id = v.organisation_id
JOIN sectors s ON s.id = v.sector_id
WHERE v.status = 'active'
  AND v.created_at &amp;gt;= NOW() - INTERVAL '30 days'
ORDER BY v.created_at DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before optimization, this query at VacancySoft returned an &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; output like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sort  (cost=15234.56..15234.61 rows=20 width=128) (actual time=1842.331..1842.345 rows=20 loops=1)
  Sort Key: v.created_at DESC
  -&amp;gt;  Hash Join  (cost=8234.12..15234.43 rows=20 width=128) (actual time=1201.442..1841.892 rows=24531 loops=1)
        -&amp;gt;  Seq Scan on vacancies v  (cost=0.00..6842.00 rows=24531 width=96) (actual time=0.021..892.134 rows=24531 loops=1)
              Filter: ((status = 'active') AND (created_at &amp;gt;= ...))
              Rows Removed by Filter: 187234
Planning Time: 0.892 ms
Execution Time: 1843.012 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two immediate red flags: a &lt;strong&gt;sequential scan&lt;/strong&gt; on the vacancies table scanning over 200,000 rows and filtering out 187,000 of them, and a &lt;strong&gt;hash join&lt;/strong&gt; that is materialising far more rows than the final result needs.&lt;/p&gt;
&lt;p&gt;This is the kind of visibility that transforms optimization from guesswork into engineering. Olamilekan Lamidi's first rule of database optimization: never optimise a query you have not profiled.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;strategy-2-indexing-strategies-the-highest-impact-change&quot;&gt;Strategy 2: Indexing Strategies — The Highest-Impact Change&lt;/h3&gt;
&lt;p&gt;Indexing is the single most impactful optimization technique in my toolkit. A well-placed index can turn a 2-second query into a 5-millisecond query. But indexes are not free — they consume storage, slow down writes, and can mislead the query planner if poorly designed.&lt;/p&gt;
&lt;h4 id=&quot;composite-indexes&quot;&gt;Composite Indexes&lt;/h4&gt;
&lt;p&gt;Order matters in composite indexes. The index &lt;code&gt;(status, created_at)&lt;/code&gt; is useful for queries that filter on &lt;code&gt;status&lt;/code&gt; and sort or filter on &lt;code&gt;created_at&lt;/code&gt;. It is useless for queries that filter only on &lt;code&gt;created_at&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For the vacancy search query above, I created:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_vacancies_status_created_at
ON vacancies (status, created_at DESC);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This single index eliminated the sequential scan. The query planner now uses an index scan that reads only the relevant rows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Index Scan using idx_vacancies_status_created_at on vacancies v
  (cost=0.42..234.56 rows=20 width=96) (actual time=0.089..2.341 rows=20 loops=1)
  Index Cond: ((status = 'active') AND (created_at &amp;gt;= ...))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execution time: from &lt;strong&gt;1,843ms to 12ms&lt;/strong&gt;. A 99.3% reduction from a single composite index.&lt;/p&gt;
&lt;h4 id=&quot;partial-indexes&quot;&gt;Partial Indexes&lt;/h4&gt;
&lt;p&gt;At VacancySoft, the majority of our queries filter on &lt;code&gt;status = 'active'&lt;/code&gt;. Only about 15% of vacancies are active at any given time. A partial index dramatically reduces the index size:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_vacancies_active_created_at
ON vacancies (created_at DESC)
WHERE status = 'active';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This index is roughly 85% smaller than the full composite index, which means it fits in memory more easily and produces faster index scans.&lt;/p&gt;
&lt;h4 id=&quot;covering-indexes-index-only-scans-&quot;&gt;Covering Indexes (Index-Only Scans)&lt;/h4&gt;
&lt;p&gt;A covering index includes all columns that a query needs, allowing PostgreSQL to serve the query entirely from the index without touching the heap (the actual table data). This eliminates random I/O, which is the most expensive operation on traditional storage.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_vacancies_covering
ON vacancies (status, created_at DESC)
INCLUDE (id, title, organisation_id, sector_id)
WHERE status = 'active';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this covering index, PostgreSQL executes an &lt;strong&gt;index-only scan&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Index Only Scan using idx_vacancies_covering on vacancies v
  (cost=0.42..45.23 rows=20 width=96) (actual time=0.067..0.412 rows=20 loops=1)
  Heap Fetches: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Heap Fetches: 0&lt;/code&gt; is the goal. The database never touches the table at all.&lt;/p&gt;
&lt;h4 id=&quot;my-indexing-methodology&quot;&gt;My Indexing Methodology&lt;/h4&gt;
&lt;p&gt;I follow a systematic process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Identify the top 20 slowest queries&lt;/strong&gt; from the slow query log.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run EXPLAIN ANALYZE&lt;/strong&gt; on each one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Look for sequential scans&lt;/strong&gt; on tables with more than 10,000 rows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check for sort operations&lt;/strong&gt; that are not backed by an index.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create targeted indexes&lt;/strong&gt; and verify the plan changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitor write performance&lt;/strong&gt; to ensure indexes are not degrading INSERT/UPDATE speed beyond acceptable thresholds.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At VacancySoft, this methodology across 15+ modules produced the 60% aggregate reduction in query execution time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;strategy-3-query-refactoring-fixing-what-the-orm-gets-wrong&quot;&gt;Strategy 3: Query Refactoring — Fixing What the ORM Gets Wrong&lt;/h3&gt;
&lt;p&gt;ORMs are excellent for productivity but frequently generate suboptimal SQL. The N+1 query problem is the most well-known example, but I encounter more subtle issues regularly.&lt;/p&gt;
&lt;h4 id=&quot;eliminating-n-1-queries&quot;&gt;Eliminating N+1 Queries&lt;/h4&gt;
&lt;p&gt;In Laravel (which I used extensively at 2am Tech, Lordwin Group, and Viaduct):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Bad: N+1 problem — 1 query for organisations + N queries for vacancies
$organisations = Organisation::all();
foreach ($organisations as $org) {
    echo $org-&amp;gt;vacancies-&amp;gt;count(); // Triggers a query per organisation
}

// Good: Eager loading — 2 queries total
$organisations = Organisation::with('vacancies')-&amp;gt;get();

// Better: When you only need the count
$organisations = Organisation::withCount('vacancies')-&amp;gt;get();
foreach ($organisations as $org) {
    echo $org-&amp;gt;vacancies_count; // No additional queries
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At 2am Tech, the Addio financial platform was suffering from response times exceeding 3 seconds on the organisation dashboard. Profiling revealed over 150 queries per page load, almost all of them N+1 queries from Eloquent relationships. I refactored the critical paths to use eager loading and &lt;code&gt;withCount&lt;/code&gt;, reducing the query count to 8 and the response time to 400ms.&lt;/p&gt;
&lt;h4 id=&quot;replacing-subqueries-with-joins-and-ctes&quot;&gt;Replacing Subqueries with JOINs and CTEs&lt;/h4&gt;
&lt;p&gt;ORMs often generate correlated subqueries where a JOIN or CTE would be far more efficient:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- ORM-generated: correlated subquery (executes once per row)
SELECT o.id, o.name,
  (SELECT COUNT(*) FROM vacancies v WHERE v.organisation_id = o.id AND v.status = 'active')
    AS active_vacancy_count
FROM organisations o
WHERE o.sector_id = 5;

-- Refactored: single pass with JOIN
SELECT o.id, o.name, COUNT(v.id) AS active_vacancy_count
FROM organisations o
LEFT JOIN vacancies v ON v.organisation_id = o.id AND v.status = 'active'
WHERE o.sector_id = 5
GROUP BY o.id, o.name;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;window-functions-for-complex-analytics&quot;&gt;Window Functions for Complex Analytics&lt;/h4&gt;
&lt;p&gt;At VacancySoft, we needed to show month-over-month vacancy trends per sector. The initial implementation used multiple queries with application-level aggregation. I replaced it with a single query using window functions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH monthly_counts AS (
  SELECT
    s.name AS sector_name,
    DATE_TRUNC('month', v.created_at) AS month,
    COUNT(*) AS vacancy_count
  FROM vacancies v
  JOIN sectors s ON s.id = v.sector_id
  WHERE v.created_at &amp;gt;= NOW() - INTERVAL '12 months'
  GROUP BY s.name, DATE_TRUNC('month', v.created_at)
)
SELECT
  sector_name,
  month,
  vacancy_count,
  LAG(vacancy_count) OVER (PARTITION BY sector_name ORDER BY month) AS prev_month_count,
  ROUND(
    (vacancy_count - LAG(vacancy_count) OVER (PARTITION BY sector_name ORDER BY month))::numeric
    / NULLIF(LAG(vacancy_count) OVER (PARTITION BY sector_name ORDER BY month), 0) * 100,
    1
  ) AS month_over_month_pct
FROM monthly_counts
ORDER BY sector_name, month;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This single query replaced five application-level queries and a significant amount of JavaScript aggregation logic. Response time for the trends endpoint dropped from 2.1 seconds to 280ms.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;strategy-4-schema-redesign-for-performance&quot;&gt;Strategy 4: Schema Redesign for Performance&lt;/h3&gt;
&lt;p&gt;Sometimes the problem is not the query — it is the schema. Normalization is taught as gospel in database courses, but production systems often require strategic denormalization.&lt;/p&gt;
&lt;h4 id=&quot;materialized-views&quot;&gt;Materialized Views&lt;/h4&gt;
&lt;p&gt;At VacancySoft, our analytics dashboard required aggregating data across five tables. Even with optimized queries and proper indexes, the aggregation was too slow for real-time display. I introduced materialized views:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE MATERIALIZED VIEW mv_sector_analytics AS
SELECT
  s.id AS sector_id,
  s.name AS sector_name,
  COUNT(v.id) AS total_vacancies,
  COUNT(v.id) FILTER (WHERE v.status = 'active') AS active_vacancies,
  COUNT(DISTINCT v.organisation_id) AS hiring_organisations,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY v.salary_max) AS median_salary,
  DATE_TRUNC('day', NOW()) AS snapshot_date
FROM sectors s
LEFT JOIN vacancies v ON v.sector_id = s.id
  AND v.created_at &amp;gt;= NOW() - INTERVAL '90 days'
GROUP BY s.id, s.name;

CREATE UNIQUE INDEX ON mv_sector_analytics (sector_id);

-- Refresh concurrently (no lock on reads)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_sector_analytics;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I set up a cron job to refresh this view every 15 minutes. The dashboard query that previously took 3.8 seconds now completes in under 10ms because it reads from the pre-computed view.&lt;/p&gt;
&lt;h4 id=&quot;strategic-denormalization&quot;&gt;Strategic Denormalization&lt;/h4&gt;
&lt;p&gt;For the organisation profile pages, rather than joining five tables on every request, I added a &lt;code&gt;cached_stats&lt;/code&gt; JSONB column on the organisations table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE organisations ADD COLUMN cached_stats JSONB DEFAULT '{}';

-- Updated by a background job every hour
UPDATE organisations SET cached_stats = jsonb_build_object(
  'active_vacancies', (SELECT COUNT(*) FROM vacancies WHERE organisation_id = organisations.id AND status = 'active'),
  'total_vacancies_90d', (SELECT COUNT(*) FROM vacancies WHERE organisation_id = organisations.id AND created_at &amp;gt;= NOW() - INTERVAL '90 days'),
  'avg_time_to_fill', (SELECT AVG(EXTRACT(EPOCH FROM (filled_at - created_at)) / 86400) FROM vacancies WHERE organisation_id = organisations.id AND filled_at IS NOT NULL),
  'updated_at', NOW()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a tradeoff: the data can be up to one hour stale, and we have the overhead of the background update job. But for this use case, slight staleness is acceptable, and the performance gain — from 450ms to 8ms per profile page — is transformative.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;strategy-5-connection-pooling-and-read-replicas&quot;&gt;Strategy 5: Connection Pooling and Read Replicas&lt;/h3&gt;
&lt;p&gt;At scale, query optimization alone is not enough. You also need to manage how connections to the database are handled.&lt;/p&gt;
&lt;h4 id=&quot;connection-pooling-with-pgbouncer&quot;&gt;Connection Pooling with PgBouncer&lt;/h4&gt;
&lt;p&gt;PostgreSQL creates a new process for each connection, which is expensive. At VacancySoft, I deployed PgBouncer in front of PostgreSQL in transaction pooling mode:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;; pgbouncer.ini
[databases]
vacancysoft = host=127.0.0.1 port=5432 dbname=vacancysoft

[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 500
default_pool_size = 25
reserve_pool_size = 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows 500 application connections to share 25 database connections. Without pooling, our application would regularly hit PostgreSQL's connection limit during traffic spikes, causing cascading failures.&lt;/p&gt;
&lt;h4 id=&quot;read-replicas&quot;&gt;Read Replicas&lt;/h4&gt;
&lt;p&gt;For read-heavy workloads (which describe most web applications), I split reads and writes across primary and replica databases:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Database configuration with read/write splitting
const knex = require('knex');

const primary = knex({
  client: 'pg',
  connection: process.env.DATABASE_PRIMARY_URL,
});

const replica = knex({
  client: 'pg',
  connection: process.env.DATABASE_REPLICA_URL,
});

// Service layer decides which connection to use
class VacancyService {
  async search(filters) {
    return replica('vacancies')
      .where(filters)
      .orderBy('created_at', 'desc');
  }

  async create(data) {
    return primary('vacancies')
      .insert(data)
      .returning('*');
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At VacancySoft, this split reduced load on the primary database by approximately 70%, since the vast majority of our traffic is read operations (search, analytics, reporting).&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;strategy-6-caching-layers&quot;&gt;Strategy 6: Caching Layers&lt;/h3&gt;
&lt;p&gt;The fastest database query is the one you never make. Caching is the final layer of my optimization strategy.&lt;/p&gt;
&lt;h4 id=&quot;redis-for-hot-data&quot;&gt;Redis for Hot Data&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

async function getSectorAnalytics(sectorId) {
  const cacheKey = `sector:${sectorId}:analytics`;
  const cached = await redis.get(cacheKey);

  if (cached) {
    return JSON.parse(cached);
  }

  const data = await db('mv_sector_analytics')
    .where('sector_id', sectorId)
    .first();

  await redis.setex(cacheKey, 900, JSON.stringify(data)); // 15-minute TTL

  return data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;cache-invalidation-strategy&quot;&gt;Cache Invalidation Strategy&lt;/h4&gt;
&lt;p&gt;Cache invalidation is famously one of the two hard problems in computer science. My approach is pragmatic:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;TTL-based expiration&lt;/strong&gt; for data that can tolerate staleness (analytics, aggregations).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event-driven invalidation&lt;/strong&gt; for data that must be fresh (user profiles, active listings). When a vacancy is created or updated, I publish an event that invalidates the relevant cache keys.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache warming&lt;/strong&gt; for predictable access patterns. At VacancySoft, I pre-warm the cache for the top 50 sectors and top 100 organisations every 15 minutes, so the first user to access these pages never experiences a cache miss.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Event-driven cache invalidation
eventBus.on('vacancy:created', async (vacancy) =&amp;gt; {
  await redis.del(`sector:${vacancy.sector_id}:analytics`);
  await redis.del(`org:${vacancy.organisation_id}:stats`);
  await redis.del('homepage:featured_vacancies');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;results-before-and-after&quot;&gt;Results: Before and After&lt;/h3&gt;
&lt;p&gt;Here is a summary of the measurable impact these strategies produced across my career, as implemented by Olamilekan Lamidi:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Company &lt;/th&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before &lt;/th&gt;&lt;th&gt;After &lt;/th&gt;&lt;th&gt;Improvement&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;VacancySoft &lt;/td&gt;&lt;td&gt;Average query execution time &lt;/td&gt;&lt;td&gt;850ms &lt;/td&gt;&lt;td&gt;340ms &lt;/td&gt;&lt;td&gt;60% reduction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;VacancySoft &lt;/td&gt;&lt;td&gt;Dashboard load time &lt;/td&gt;&lt;td&gt;3.8s &lt;/td&gt;&lt;td&gt;0.4s &lt;/td&gt;&lt;td&gt;89% reduction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;VacancySoft &lt;/td&gt;&lt;td&gt;p95 API response time &lt;/td&gt;&lt;td&gt;1.2s &lt;/td&gt;&lt;td&gt;180ms &lt;/td&gt;&lt;td&gt;85% reduction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Univelcity &lt;/td&gt;&lt;td&gt;API response time (avg) &lt;/td&gt;&lt;td&gt;620ms &lt;/td&gt;&lt;td&gt;340ms &lt;/td&gt;&lt;td&gt;45% reduction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Univelcity &lt;/td&gt;&lt;td&gt;Queries per page load &lt;/td&gt;&lt;td&gt;45 &lt;/td&gt;&lt;td&gt;8 &lt;/td&gt;&lt;td&gt;82% reduction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Lordwin Group &lt;/td&gt;&lt;td&gt;Real-time query latency &lt;/td&gt;&lt;td&gt;220ms &lt;/td&gt;&lt;td&gt;35ms &lt;/td&gt;&lt;td&gt;84% reduction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2am Tech &lt;/td&gt;&lt;td&gt;Organisation dashboard load &lt;/td&gt;&lt;td&gt;3.1s &lt;/td&gt;&lt;td&gt;400ms &lt;/td&gt;&lt;td&gt;87% reduction&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Profile before you optimize.&lt;/strong&gt; EXPLAIN ANALYZE and slow query logs should be your starting point, not your intuition.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composite and partial indexes are the highest-leverage optimization&lt;/strong&gt; in most applications. One well-placed index can eliminate 99% of query time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ORMs are productivity tools, not performance tools.&lt;/strong&gt; Review the SQL they generate, especially for complex queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Materialized views and strategic denormalization&lt;/strong&gt; are not anti-patterns — they are essential tools for read-heavy workloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection pooling is mandatory&lt;/strong&gt; once you are handling more than a few hundred concurrent connections.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching is the last layer&lt;/strong&gt;, not the first. Optimize the underlying queries before adding cache complexity.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Database optimization is not a one-time project. It is an ongoing discipline. The queries that perform well today may become bottlenecks tomorrow as data grows and access patterns change. Build profiling and monitoring into your workflow, and treat database performance as a first-class engineering concern.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14921/database-optimization-strategies-that-cut-query-execution-time-by-60-a-practical-guide</guid>
<pubDate>Wed, 15 Apr 2026 23:39:07 +0000</pubDate>
</item>
<item>
<title>From Zero to Hero: Create a Cyberpunk Snake Game with Real-Time Ranking using Supabase and Vanilla J</title>
<link>https://coderlegion.com/12990/from-zero-hero-create-cyberpunk-snake-game-with-real-time-ranking-using-supabase-vanilla</link>
<description>&lt;p&gt;Do you remember the classic Snake from the old Nokias? Now imagine it with neon lights, synthesized music, explosive particles, and most importantly: &lt;strong&gt;connected to the cloud&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Web game development has experienced a renaissance thanks to the power of HTML5 Canvas and &lt;strong&gt;Serverless&lt;/strong&gt; tools. In the past, if you wanted to save your players' scores to create a global leaderboard, you needed to set up a server, configure a REST API, manage databases, and pay for hosting. Today, thanks to Backend-as-a-Service (BaaS), we can do it in minutes.&lt;/p&gt;
&lt;p&gt;In this tutorial, we are going to build &lt;strong&gt;&quot;Neon Snake&quot;&lt;/strong&gt;, an aesthetic and functional web arcade game that uses &lt;strong&gt;Supabase&lt;/strong&gt; (an Open Source alternative to Firebase) to persist data in PostgreSQL.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/game_snake/imagen_snake_game.png&quot; alt=&quot;Conceptual image of Snake Game&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;-the-tech-stack&quot;&gt;️ The Tech Stack&lt;/h3&gt;
&lt;p&gt;We're going to keep it simple but powerful. No heavy frameworks, just the purity of the web:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; HTML5 Canvas + Vanilla JavaScript (ES6+).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Styles:&lt;/strong&gt; Tailwind CSS (via CDN) for the user interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Supabase (PostgreSQL + Realtime).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audio:&lt;/strong&gt; Web Audio API for code-generated sound effects.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-setting-up-the-backend-with-supabase&quot;&gt;Step 1: Setting up the Backend with Supabase&lt;/h3&gt;
&lt;p&gt;Before writing a single line of JavaScript, we need our brain in the cloud. Supabase offers us a real PostgreSQL database with an automatically generated API.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a free account at &lt;a rel=&quot;nofollow&quot; href=&quot;https://supabase.com&quot;&gt;supabase.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create a new project called &lt;code&gt;NeonSnake&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Table Editor&lt;/strong&gt; section and create a new table called &lt;code&gt;snake_scores&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-magic-of-sql&quot;&gt;The Magic of SQL&lt;/h4&gt;
&lt;p&gt;To streamline the process, Supabase allows you to execute SQL commands directly. Go to the &lt;strong&gt;SQL Editor&lt;/strong&gt; and paste the following code. This will create the table structure and configure the security policies (RLS - Row Level Security) so your game can read and write data publicly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 1. Create the scores table
create table snake_scores (
  id bigint generated by default as identity primary key,
  username text not null,
  score integer not null,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null
);

-- 2. Enable Row Level Security (RLS)
alter table snake_scores enable row level security;

-- 3. Policy to allow anyone to view the ranking (Public Read)
create policy &quot;Ver ranking publico&quot;
on snake_scores for select
using ( true );

-- 4. Policy to allow anyone to save their record (Public Write)
create policy &quot;Guardar puntuacion&quot;
on snake_scores for insert
with check ( true );
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Security Note:&lt;/strong&gt; In a commercial game, we would use full user authentication. For this arcade-style demo, we allow public (anonymous) access to reduce friction so anyone can play and log their record instantly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Finally, go to &lt;strong&gt;Settings &amp;gt; API&lt;/strong&gt; and copy your &lt;code&gt;Project URL&lt;/code&gt; and your &lt;code&gt;anon public key&lt;/code&gt;. You will need them shortly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-the-cyberpunk-frontend-html5-canvas-&quot;&gt;Step 2: The Cyberpunk Frontend (HTML5 + Canvas)&lt;/h3&gt;
&lt;p&gt;Our game will live in a single &lt;code&gt;index.html&lt;/code&gt; file. We will use &lt;strong&gt;Tailwind CSS&lt;/strong&gt; for the floating menus (Game Over, Start Screen) and the &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element to render the game at 60 FPS.&lt;/p&gt;
&lt;p&gt;The basic structure is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Neon Snake 2077&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;[https://cdn.tailwindcss.com](https://cdn.tailwindcss.com)&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;style&amp;gt;
        @import url('[https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&amp;amp;display=swap](https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&amp;amp;display=swap)');
        body { font-family: 'Orbitron', sans-serif; background-color: #050505; }
        /* Neon glow effect for the canvas */
        canvas { box-shadow: 0 0 20px #0ff; border: 2px solid #0ff; }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body class=&quot;h-screen w-full flex items-center justify-center overflow-hidden bg-gray-900&quot;&amp;gt;

    &amp;lt;div class=&quot;relative&quot;&amp;gt;
        &amp;lt;canvas id=&quot;gameCanvas&quot; width=&quot;400&quot; height=&quot;400&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
        
        &amp;lt;div id=&quot;gameOverMenu&quot; class=&quot;hidden absolute inset-0 bg-black/90 flex flex-col items-center justify-center z-10&quot;&amp;gt;
            &amp;lt;h2 class=&quot;text-4xl text-red-500 font-bold mb-4 drop-shadow-[0_0_10px_rgba(239,68,68,0.8)]&quot;&amp;gt;SYSTEM FAILURE&amp;lt;/h2&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;usernameInput&quot; placeholder=&quot;Enter ID...&quot; class=&quot;bg-gray-800 text-cyan-400 p-2 rounded border border-cyan-500 mb-4 text-center outline-none&quot;&amp;gt;
            &amp;lt;button id=&quot;saveBtn&quot; class=&quot;bg-cyan-600 hover:bg-cyan-500 text-white font-bold py-2 px-4 rounded shadow-[0_0_15px_#06b6d4] transition-all&quot;&amp;gt;
                UPLOAD DATA
            &amp;lt;/button&amp;gt;
            
            &amp;lt;div id=&quot;leaderboard&quot; class=&quot;mt-6 text-sm text-gray-300 w-64 text-left h-40 overflow-y-auto custom-scrollbar&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script type=&quot;module&quot;&amp;gt;
        import { createClient } from '[https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm](https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm)'
        
        // ... Game Logic here (See step 3) ...
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;game-logic-beyond-eating-apples&quot;&gt;Game Logic: Beyond Eating Apples&lt;/h4&gt;
&lt;p&gt;To give it that &quot;Pro&quot; touch and differentiate it from a basic tutorial, we implement several key mechanics in JavaScript:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Animation Loop:&lt;/strong&gt; Using &lt;code&gt;requestAnimationFrame&lt;/code&gt; to maintain fluidity at 60 FPS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Particle System:&lt;/strong&gt; An array of objects that spawn at the &quot;food&quot; coordinates when eaten and fade gradually, simulating a data explosion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Firewalls:&lt;/strong&gt; From level 5 onwards, we generate static obstacles that the player must dodge, progressively increasing the difficulty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid Controls:&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PC:&lt;/strong&gt; We listen for &lt;code&gt;keydown&lt;/code&gt; events for directional arrows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile:&lt;/strong&gt; We capture &lt;code&gt;touchstart&lt;/code&gt; and &lt;code&gt;touchend&lt;/code&gt; to calculate the gesture direction (&lt;em&gt;swipe&lt;/em&gt;) and move the snake.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-connecting-the-dots-integration-with-supabase-&quot;&gt;Step 3: Connecting the Dots (Integration with Supabase)&lt;/h3&gt;
&lt;p&gt;Here is where the Backend-as-a-Service magic happens. We are going to connect our Canvas to the PostgreSQL database. Inside our &lt;code&gt;&amp;lt;script type=&quot;module&quot;&amp;gt;&lt;/code&gt; tag:&lt;/p&gt;
&lt;h4 id=&quot;1-client-initialization&quot;&gt;1. Client Initialization&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const supabaseUrl = 'YOUR_SUPABASE_PROJECT_URL';
const supabaseKey = 'YOUR_SUPABASE_ANON_KEY';
const supabase = createClient(supabaseUrl, supabaseKey);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-the-game-over-flow&quot;&gt;2. The &quot;Game Over&quot; Flow&lt;/h4&gt;
&lt;p&gt;When the player loses and hits &quot;UPLOAD DATA&quot;, we need to ensure the data is saved before showing the leaderboard. We will use &lt;code&gt;async/await&lt;/code&gt; to control this asynchronous flow.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function saveScore(username, score) {
    // 1. Save the score
    const { error } = await supabase
        .from('snake_scores')
        .insert([{ username: username, score: score }]);

    if (error) {
        console.error('Error uploading data:', error);
        alert('Connection error with the central server.');
        return;
    }
    
    // 2. Immediately after, update the leaderboard visually
    fetchLeaderboard();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-fetching-the-ranking-select-&quot;&gt;3. Fetching the Ranking (Select)&lt;/h4&gt;
&lt;p&gt;We retrieve the &lt;strong&gt;Top 20&lt;/strong&gt; sorted by score in descending order to show who rules the server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function fetchLeaderboard() {
    const { data: scores, error } = await supabase
        .from('snake_scores')
        .select('username, score')
        .order('score', { ascending: false }) // Descending order
        .limit(20);                           // Only the top 20

    if (error) console.error('Error fetching leaderboard:', error);
    
    // Render in the DOM
    const list = document.getElementById('leaderboard');
    list.innerHTML = '&amp;lt;h3 class=&quot;text-cyan-400 border-b border-cyan-500 mb-2 font-bold&quot;&amp;gt;TOP HACKERS&amp;lt;/h3&amp;gt;';
    
    scores.forEach((entry, index) =&amp;gt; {
        // Highlight top 3 with different colors if you want
        const colorClass = index &amp;lt; 3 ? 'text-yellow-400' : 'text-gray-300';
        
        list.innerHTML += `
            &amp;lt;div class=&quot;flex justify-between py-1 border-b border-gray-800&quot;&amp;gt;
                &amp;lt;span class=&quot;${colorClass}&quot;&amp;gt;#${index + 1} ${entry.username}&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;text-cyan-300 font-mono&quot;&amp;gt;${entry.score}&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;`;
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;bonus-realtime-&quot;&gt;Bonus: Realtime ⚡&lt;/h4&gt;
&lt;p&gt;Do you want the ranking to update on &lt;strong&gt;everyone's&lt;/strong&gt; screen if someone breaks the record at that precise moment? Supabase makes this trivial with its subscriptions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Subscribe to changes in the 'snake_scores' table
supabase
  .channel('public:snake_scores')
  .on('postgres_changes', 
      { event: 'INSERT', schema: 'public', table: 'snake_scores' }, 
      (payload) =&amp;gt; {
        console.log('New record detected on the network!', payload);
        fetchLeaderboard(); // Automatically refresh the list for everyone
      }
  )
  .subscribe();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-final-result&quot;&gt;The Final Result&lt;/h3&gt;
&lt;p&gt;By putting it all together, you get a fluid and highly competitive user experience:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user plays frantically dodging neon &quot;Firewalls&quot;.&lt;/li&gt;
&lt;li&gt;Upon crashing, the game stops with a visual &quot;glitch&quot; effect.&lt;/li&gt;
&lt;li&gt;They enter their name (e.g., &quot;Neo&quot;).&lt;/li&gt;
&lt;li&gt;Upon clicking send, &lt;strong&gt;in milliseconds&lt;/strong&gt;, their name appears on the leaderboard.&lt;/li&gt;
&lt;li&gt;If their friend is playing on another mobile at the same time, they will see &quot;Neo's&quot; name appear on their screen without reloading the page.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And once created... all that remains is to enjoy and play &lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/games/snake/&quot;&gt;Datalaria Snake Game&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;We have moved from a simple static canvas to a &lt;strong&gt;Fullstack&lt;/strong&gt; application in real-time without touching a traditional backend server (Node.js, PHP, Python) or managing infrastructure. The combination of creativity in game development with the power of BaaS tools like Supabase opens a world of possibilities for frontend developers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The next step?&lt;/strong&gt; I invite you to clone the project and improve it: add GitHub authentication to avoid duplicate names, create snake skins that unlock with high scores, or implement a multiplayer mode using Supabase presence channels.&lt;/p&gt;
&lt;p&gt;The code is yours! &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Did you like this tutorial? Share it on your networks with your High Scores and don't forget to check the official Supabase documentation to dive deeper.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/12990/from-zero-hero-create-cyberpunk-snake-game-with-real-time-ranking-using-supabase-vanilla</guid>
<pubDate>Tue, 17 Mar 2026 06:55:03 +0000</pubDate>
</item>
<item>
<title>Getting Started with PostgreSQL: Best Practices from Development to Production</title>
<link>https://coderlegion.com/12899/getting-started-with-postgresql-best-practices-from-development-to-production</link>
<description>&lt;p&gt;Databases are the backbone of modern applications. Whether you're building APIs, SaaS platforms, or enterprise systems, choosing the right database and using it correctly can make a huge difference in performance and scalability.&lt;/p&gt;
&lt;p&gt;One of the most reliable and powerful databases available today is &lt;strong&gt;PostgreSQL&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;PostgreSQL is known for its &lt;strong&gt;stability, extensibility, performance, and strong SQL compliance&lt;/strong&gt;, which is why many modern systems rely on it for production workloads.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll explore how to &lt;strong&gt;get started with PostgreSQL and follow best practices from development all the way to production environments&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-choose-postgresql-&quot;&gt;Why Choose PostgreSQL?&lt;/h2&gt;
&lt;p&gt;PostgreSQL has become a favorite among developers and companies because it offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced SQL support&lt;/li&gt;
&lt;li&gt;Strong data integrity&lt;/li&gt;
&lt;li&gt;JSON support for hybrid workloads&lt;/li&gt;
&lt;li&gt;Extensibility through extensions&lt;/li&gt;
&lt;li&gt;Excellent performance for large-scale systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many major platforms rely on PostgreSQL, including &lt;strong&gt;Netflix&lt;/strong&gt;, &lt;strong&gt;Apple&lt;/strong&gt;, and &lt;strong&gt;Instagram&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It works well for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;web applications&lt;/li&gt;
&lt;li&gt;analytics workloads&lt;/li&gt;
&lt;li&gt;enterprise systems&lt;/li&gt;
&lt;li&gt;microservices architectures&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;installing-postgresql&quot;&gt;Installing PostgreSQL&lt;/h2&gt;
&lt;p&gt;You can install PostgreSQL locally using the official installer or through a package manager.&lt;/p&gt;
&lt;p&gt;Typical installation steps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS (Homebrew)
brew install postgresql

# Ubuntu
sudo apt install postgresql

# Windows
Download installer from postgresql.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After installation, start the database service and connect using the CLI tool:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql -U postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;basic-database-setup&quot;&gt;Basic Database Setup&lt;/h2&gt;
&lt;p&gt;A typical project setup includes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE DATABASE myapp;

CREATE USER myapp_user WITH PASSWORD 'securepassword';

GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp_user;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation between &lt;strong&gt;database owner and application user&lt;/strong&gt; is an important security best practice.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;designing-tables-properly&quot;&gt;Designing Tables Properly&lt;/h2&gt;
&lt;p&gt;Good database design is essential for long-term scalability.&lt;/p&gt;
&lt;p&gt;Example table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Important principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;always define a &lt;strong&gt;primary key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;enforce &lt;strong&gt;unique constraints where necessary&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;avoid nullable fields unless truly optional&lt;/li&gt;
&lt;li&gt;use timestamps for auditing&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-indexes-for-performance&quot;&gt;Use Indexes for Performance&lt;/h2&gt;
&lt;p&gt;Indexes help speed up queries significantly.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_email ON users(email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Indexes are useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;search queries&lt;/li&gt;
&lt;li&gt;filtering large datasets&lt;/li&gt;
&lt;li&gt;join operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, too many indexes can slow down writes, so use them strategically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-migrations-instead-of-manual-changes&quot;&gt;Use Migrations Instead of Manual Changes&lt;/h2&gt;
&lt;p&gt;Schema changes should always be version-controlled.&lt;/p&gt;
&lt;p&gt;Popular migration tools include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prisma&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeORM&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flyway&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example migration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE users ADD COLUMN last_login TIMESTAMP;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures that all environments stay &lt;strong&gt;consistent&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-transactions-for-data-safety&quot;&gt;Use Transactions for Data Safety&lt;/h2&gt;
&lt;p&gt;Transactions ensure that multiple operations succeed or fail together.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;BEGIN;

INSERT INTO orders (...) VALUES (...);
UPDATE users SET balance = balance - 100 WHERE id = 1;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If something fails:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transactions prevent &lt;strong&gt;partial updates and inconsistent data&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;use-connection-pooling&quot;&gt;Use Connection Pooling&lt;/h2&gt;
&lt;p&gt;Opening a database connection for every request is expensive.&lt;/p&gt;
&lt;p&gt;Connection pooling improves performance by reusing connections.&lt;/p&gt;
&lt;p&gt;Popular tools include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PgBouncer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pg-pool&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Connection pooling becomes essential in &lt;strong&gt;high traffic production systems&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;logging-and-monitoring&quot;&gt;Logging and Monitoring&lt;/h2&gt;
&lt;p&gt;Monitoring your database helps detect issues early.&lt;/p&gt;
&lt;p&gt;PostgreSQL provides built-in logging capabilities.&lt;/p&gt;
&lt;p&gt;You can monitor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slow queries&lt;/li&gt;
&lt;li&gt;long-running transactions&lt;/li&gt;
&lt;li&gt;connection usage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tools commonly used with PostgreSQL include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pgAdmin&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grafana&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Monitoring is crucial for production reliability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;backup-strategy-for-production&quot;&gt;Backup Strategy for Production&lt;/h2&gt;
&lt;p&gt;Always have automated backups.&lt;/p&gt;
&lt;p&gt;Typical strategies include:&lt;/p&gt;
&lt;p&gt;Daily backups:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pg_dump myapp &amp;gt; backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full database backups:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pg_dumpall &amp;gt; full_backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For production systems, consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;automated cloud backups&lt;/li&gt;
&lt;li&gt;replication clusters&lt;/li&gt;
&lt;li&gt;point-in-time recovery&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This protects your data from accidental loss.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;scaling-postgresql-in-production&quot;&gt;Scaling PostgreSQL in Production&lt;/h2&gt;
&lt;p&gt;As your application grows, scaling becomes important.&lt;/p&gt;
&lt;p&gt;Common scaling strategies include:&lt;/p&gt;
&lt;h4 id=&quot;read-replicas-1&quot;&gt;Read Replicas&lt;/h4&gt;
&lt;p&gt;Separate read queries from write operations.&lt;/p&gt;
&lt;p&gt;Example architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Primary DB → Write operations
Replica DB → Read queries
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This improves performance under heavy load.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;partitioning-large-tables&quot;&gt;Partitioning Large Tables&lt;/h4&gt;
&lt;p&gt;When tables become very large, partitioning improves performance.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE logs_2025 PARTITION OF logs
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Partitioning helps with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;faster queries&lt;/li&gt;
&lt;li&gt;easier data management&lt;/li&gt;
&lt;li&gt;improved maintenance&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-best-practices&quot;&gt;Security Best Practices&lt;/h2&gt;
&lt;p&gt;Production databases must be secured properly.&lt;/p&gt;
&lt;p&gt;Important steps include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;enforce strong passwords&lt;/li&gt;
&lt;li&gt;restrict database network access&lt;/li&gt;
&lt;li&gt;use SSL connections&lt;/li&gt;
&lt;li&gt;avoid superuser access for applications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Principle of &lt;strong&gt;least privilege&lt;/strong&gt; is essential.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;development-vs-production-setup&quot;&gt;Development vs Production Setup&lt;/h2&gt;
&lt;p&gt;Development environments can be simple, but production requires more structure.&lt;/p&gt;
&lt;p&gt;Development setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;local PostgreSQL instance&lt;/li&gt;
&lt;li&gt;minimal security configuration&lt;/li&gt;
&lt;li&gt;small datasets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Production setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;managed PostgreSQL service&lt;/li&gt;
&lt;li&gt;automated backups&lt;/li&gt;
&lt;li&gt;monitoring and alerts&lt;/li&gt;
&lt;li&gt;connection pooling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many companies use managed services like &lt;strong&gt;Amazon RDS&lt;/strong&gt; or &lt;strong&gt;Azure Database for PostgreSQL&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;PostgreSQL is one of the most &lt;strong&gt;powerful and reliable databases available today&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But simply installing it is not enough. Building scalable systems requires:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;proper schema design&lt;/li&gt;
&lt;li&gt;indexing strategies&lt;/li&gt;
&lt;li&gt;migration workflows&lt;/li&gt;
&lt;li&gt;monitoring and backups&lt;/li&gt;
&lt;li&gt;production-grade security&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When used correctly, PostgreSQL can power applications from &lt;strong&gt;small startups to massive enterprise platforms&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you're building modern applications today, PostgreSQL is definitely a database worth mastering.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.enterprisedb.com/downloads/postgres-postgresql-downloads&quot;&gt;https://www.enterprisedb.com/downloads/postgres-postgresql-downloads&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12899/getting-started-with-postgresql-best-practices-from-development-to-production</guid>
<pubDate>Sun, 15 Mar 2026 17:40:28 +0000</pubDate>
</item>
<item>
<title>Merancang Dashboard Manajemen Proyek Konstruksi: Progress, RAB, dan Issue Tracking Berbasis Web</title>
<link>https://coderlegion.com/12133/merancang-dashboard-manajemen-proyek-konstruksi-progress-rab-issue-tracking-berbasis</link>
<description>&lt;p&gt;Di 2026, obrolan soal &lt;em&gt;AI agents&lt;/em&gt; mulai masuk ke meja operasional konstruksi—bukan lagi sekadar demo, tapi cara baru mengorkestrasi data lintas tim (lapangan–QS–procurement–PMO). Bahkan media bisnis besar menyorot peluangnya di industri konstruksi lewat pembahasan tentang &lt;em&gt;AI agents&lt;/em&gt; dan produktivitas proyek di artikel &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.wsj.com/articles/construction-companies-see-promise-in-ai-agents-12dc2d60&quot;&gt;AI agents untuk konstruksi&lt;/a&gt;. Namun sebelum “AI” bekerja, fondasinya harus beres: data progres, biaya, dan isu harus bisa bicara lewat API. Di sinilah &lt;em&gt;dashboard manajemen proyek konstruksi&lt;/em&gt; jadi pintu masuk paling masuk akal.&lt;/p&gt;
&lt;p&gt;Kalau kamu pernah merasa progres “aman” tapi cashflow meledak—kamu tidak sendirian. Riset tentang pelacakan dan analisis real-time di proyek konstruksi menegaskan tantangan utama ada pada data multi-sumber (jadwal, BIM, platform manajemen proyek, hingga data lapangan) yang terlambat terhubung ke keputusan harian. Itu dibahas lebih teknis di studi &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sciencedirect.com/science/article/pii/S1474034625004045&quot;&gt;real-time tracking and analysis in construction projects&lt;/a&gt;. Kita mengangkat tema ini karena banyak developer membangun aplikasi “rapi”, tapi jarang menyentuh problem yang benar-benar menyelamatkan proyek: &lt;em&gt;single source of truth&lt;/em&gt; + metrik yang disepakati.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Dashboard bukan pajangan. Ia adalah kontrak: definisi metrik, ritme data masuk, dan cara tim mengambil keputusan.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-kenapa-dashboard-itu-lebih-dari-grafik&quot;&gt;1. Kenapa Dashboard Itu Lebih dari Grafik&lt;/h3&gt;
&lt;p&gt;Dashboard yang dipakai manajemen bukan yang paling cantik—melainkan yang paling &lt;em&gt;actionable&lt;/em&gt;. Di konstruksi, &lt;em&gt;actionable&lt;/em&gt; artinya: begitu ada deviasi, orang tahu &lt;strong&gt;harus ngapain&lt;/strong&gt;, bukan cuma “melihat angka”. Pembukaan bab ini sengaja menekankan bahwa &lt;em&gt;dashboard manajemen proyek konstruksi&lt;/em&gt; adalah produk data + proses, bukan semata UI.&lt;/p&gt;
&lt;h4 id=&quot;tiga-pertanyaan-yang-wajib-dijawab-dashboard&quot;&gt;Tiga pertanyaan yang wajib dijawab dashboard&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Progress:&lt;/strong&gt; “Apa yang benar-benar sudah selesai?” (volume, bobot, &lt;em&gt;earned value&lt;/em&gt; sederhana)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Biaya (RAB vs aktual):&lt;/strong&gt; “Apakah biaya mengikuti rencana?” (burn rate, deviasi per item)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issue tracking:&lt;/strong&gt; “Apa yang menghambat dan siapa owner-nya?” (RFI, NCR, HSE finding, approval delay)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;pola-gagal-yang-sering-terjadi&quot;&gt;Pola gagal yang sering terjadi&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Progres ada di laporan harian, biaya ada di spreadsheet, issue ada di WhatsApp.&lt;/li&gt;
&lt;li&gt;Definisi persentase berbeda antar tim (“80%” versi site ≠ “80%” versi QS).&lt;/li&gt;
&lt;li&gt;Dashboard lambat → ditinggal → kembali ke chat/Excel.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-peta-data-dari-lapangan-ke-single-source-of-truth&quot;&gt;2. Peta Data: Dari Lapangan ke Single Source of Truth&lt;/h3&gt;
&lt;p&gt;Sebelum bicara Next.js atau Postgres, tentukan dulu “bahasa data”-nya. Bab ini membuka dengan satu prinsip: kalau input berantakan, output secanggih apa pun akan jadi noise. Dengan begitu, &lt;em&gt;dashboard manajemen proyek konstruksi&lt;/em&gt; bisa tumbuh stabil.&lt;/p&gt;
&lt;h4 id=&quot;minimal-dataset-yang-worth-collecting&quot;&gt;Minimal dataset yang &lt;em&gt;worth collecting&lt;/em&gt;&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Domain       &lt;/th&gt;&lt;th&gt;Contoh data                   &lt;/th&gt;&lt;th&gt;Sumber umum             &lt;/th&gt;&lt;th&gt;Frekuensi       &lt;/th&gt;&lt;th&gt;Risiko kalau tidak rapi&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Progres      &lt;/td&gt;&lt;td&gt;volume, bobot item, % capaian &lt;/td&gt;&lt;td&gt;daily report, checklist &lt;/td&gt;&lt;td&gt;harian          &lt;/td&gt;&lt;td&gt;% “ngambang”, sulit audit&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;RAB          &lt;/td&gt;&lt;td&gt;struktur budget, baseline     &lt;/td&gt;&lt;td&gt;RAB/BoQ                 &lt;/td&gt;&lt;td&gt;awal + revisi   &lt;/td&gt;&lt;td&gt;deviasi tidak terlacak&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Biaya aktual &lt;/td&gt;&lt;td&gt;PO, invoice, petty cash       &lt;/td&gt;&lt;td&gt;finance/procurement     &lt;/td&gt;&lt;td&gt;harian/mingguan &lt;/td&gt;&lt;td&gt;overrun ketahuan telat&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Issue        &lt;/td&gt;&lt;td&gt;RFI, NCR, HSE finding         &lt;/td&gt;&lt;td&gt;site + QA/QC            &lt;/td&gt;&lt;td&gt;harian          &lt;/td&gt;&lt;td&gt;bottleneck jadi laten&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Resource     &lt;/td&gt;&lt;td&gt;man power, alat, material     &lt;/td&gt;&lt;td&gt;log lapangan            &lt;/td&gt;&lt;td&gt;harian          &lt;/td&gt;&lt;td&gt;produktivitas tidak kebaca&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;kontrak-input-yang-realistis-bukan-utopis-&quot;&gt;Kontrak input yang realistis (bukan utopis)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Daily report&lt;/strong&gt;: tanggal, cuaca, shift, item pekerjaan + volume, foto, kendala&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost ledger&lt;/strong&gt;: tanggal, item, vendor, nilai, tipe (commitment vs actual)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issue&lt;/strong&gt;: tipe, severity, owner, due date, status, evidence&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;Tip praktis: “Wajibkan 5–7 field, bukan 20 field.” Sistem yang memaksa detail berlebihan biasanya ditinggalkan.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-arsitektur-mvp-yang-developer-friendly&quot;&gt;3. Arsitektur MVP yang Developer-Friendly&lt;/h3&gt;
&lt;p&gt;Kita akan gunakan stack yang &lt;em&gt;mainstream&lt;/em&gt; di komunitas: Next.js + TypeScript untuk UI, API layer (Next API Routes atau service terpisah), dan PostgreSQL untuk data. Fokusnya: &lt;em&gt;agregasi&lt;/em&gt;—karena dashboard hidup dari ringkasan.&lt;/p&gt;
&lt;h4 id=&quot;komponen&quot;&gt;Komponen&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js (App Router), TypeScript, TanStack Query&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend:&lt;/strong&gt; API (REST), validation (Zod/Pydantic), auth (JWT/Session)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DB:&lt;/strong&gt; PostgreSQL (schema untuk progres, RAB, biaya, issue)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability:&lt;/strong&gt; OpenTelemetry + log correlation (request-id)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optional:&lt;/strong&gt; background worker (BullMQ/Temporal) untuk ETL ringan&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;kenapa-endpoint-agregasi-itu-kunci&quot;&gt;Kenapa “endpoint agregasi” itu kunci&lt;/h4&gt;
&lt;p&gt;Kalau UI harus menarik data mentah lalu menghitung sendiri, lama-lama jadi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;query berat di client&lt;/li&gt;
&lt;li&gt;duplikasi logic&lt;/li&gt;
&lt;li&gt;data “tidak konsisten” antar halaman&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Solusi: buat endpoint seperti &lt;code&gt;GET /projects/:id/dashboard&lt;/code&gt; yang mengirim:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KPI ringkas&lt;/li&gt;
&lt;li&gt;series chart&lt;/li&gt;
&lt;li&gt;daftar deviasi terbesar&lt;/li&gt;
&lt;li&gt;issue prioritas (SLA breach)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bacaan internal yang relevan (dan sedang ramai di CoderLegion) soal menyatukan progres, RAB, dan laporan ke satu alur data bisa kamu bandingkan dengan artikel &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/12111/membangun-dashboard-monitoring-proyek-konstruksi-dengan-python-fastapi-progress-lapor&quot;&gt;Dashboard Monitoring Proyek Konstruksi dengan Python + FastAPI&lt;/a&gt; dari penulis aktif.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-model-data-skema-yang-tidak-membuat-kamu-menyesal&quot;&gt;4. Model Data: Skema yang Tidak Membuat Kamu Menyesal&lt;/h3&gt;
&lt;p&gt;Kebanyakan dashboard mati bukan karena UI jelek—tapi karena skema DB menyulitkan agregasi. Bab ini membuka dengan pola desain yang “tahan revisi” ketika RAB berubah atau definisi progres disesuaikan.&lt;/p&gt;
&lt;h4 id=&quot;entitas-inti-versi-praktis-&quot;&gt;Entitas inti (versi praktis)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;projects&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;work_items&lt;/code&gt; (item pekerjaan + bobot + unit)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;budget_items&lt;/code&gt; (struktur RAB/BoQ)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;daily_reports&lt;/code&gt; (header laporan)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;daily_progress&lt;/code&gt; (detail progres per item per hari)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cost_ledger&lt;/code&gt; (commitment &amp;amp; actual)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;issues&lt;/code&gt; (RFI/NCR/HSE/approval)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;prinsip-penting&quot;&gt;Prinsip penting&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pisahkan header vs detail&lt;/strong&gt; (laporan harian jangan jadi 1 tabel raksasa)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simpan unit &amp;amp; koefisien&lt;/strong&gt; agar konversi tidak “liar” di UI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versioning RAB&lt;/strong&gt; (&lt;code&gt;budget_version&lt;/code&gt;) untuk audit dan perubahan scope&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-how-to-dari-nol-ke-dashboard-yang-data-ready&quot;&gt;5. How-To: Dari Nol ke Dashboard yang Data-Ready&lt;/h3&gt;
&lt;p&gt;Bab ini sengaja dibuat taktis. Tujuannya: kamu bisa mulai dari “MVP yang dipakai”, lalu iterasi. Minimal 1 sprint.&lt;/p&gt;
&lt;h4 id=&quot;langkah-1-sepakati-metrik-biar-tidak-debat-di-akhir-&quot;&gt;Langkah 1 — Sepakati metrik (biar tidak debat di akhir)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Definisikan &lt;em&gt;progress&lt;/em&gt; (volume, bobot, atau EVM sederhana?)&lt;/li&gt;
&lt;li&gt;Definisikan “aktual biaya” (cash out vs commitment?)&lt;/li&gt;
&lt;li&gt;Definisikan prioritas issue (severity + SLA)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;langkah-2-buat-form-input-harian-yang-mobile-friendly&quot;&gt;Langkah 2 — Buat form input harian yang mobile-friendly&lt;/h4&gt;
&lt;p&gt;Checklist form:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tanggal, shift, cuaca&lt;/li&gt;
&lt;li&gt;item + volume + evidence (foto)&lt;/li&gt;
&lt;li&gt;resource (man power, alat)&lt;/li&gt;
&lt;li&gt;kendala + kategori&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;langkah-3-bangun-api-kontrak-yang-stabil&quot;&gt;Langkah 3 — Bangun API kontrak yang stabil&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Validasi ketat (Zod/Pydantic)&lt;/li&gt;
&lt;li&gt;Idempotency key untuk submit ulang&lt;/li&gt;
&lt;li&gt;Error format konsisten (&lt;code&gt;code&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;hint&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;langkah-4-buat-endpoint-agregasi-untuk-dashboard&quot;&gt;Langkah 4 — Buat endpoint agregasi untuk dashboard&lt;/h4&gt;
&lt;p&gt;Contoh output (ringkas):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kpis&lt;/code&gt;: progress total, deviasi biaya, burn rate&lt;/li&gt;
&lt;li&gt;&lt;code&gt;charts&lt;/code&gt;: progress series, cost series&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alerts&lt;/code&gt;: top deviasi item, issue SLA breach&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;langkah-5-tambahkan-audit-trail&quot;&gt;Langkah 5 — Tambahkan audit trail&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;created_by&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;histori revisi daily report&lt;/li&gt;
&lt;li&gt;change log untuk budget version&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;Micro-win: begitu tim bisa melihat “deviasi per item” + “issue yang menahan progres”, adopsi dashboard biasanya naik drastis.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-taktik-ux-yang-bikin-dashboard-dipakai-bukan-sekali-lihat-&quot;&gt;6. Taktik UX yang Bikin Dashboard Dipakai (Bukan Sekali Lihat)&lt;/h3&gt;
&lt;p&gt;Bab ini dibuka dengan realita: user dashboard di proyek itu sibuk. Mereka perlu “jawaban cepat”, bukan eksplorasi panjang. Kita tetap ingat konteks &lt;em&gt;dashboard manajemen proyek konstruksi&lt;/em&gt;—harus terasa seperti alat kerja.&lt;/p&gt;
&lt;h4 id=&quot;pola-layout-yang-terbukti-efektif&quot;&gt;Pola layout yang terbukti efektif&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Top bar KPI (3–5 angka):&lt;/strong&gt; progress, deviasi biaya, issue open, SLA breach&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Middle:&lt;/strong&gt; 2 chart utama (progress vs time, cost vs time)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bottom:&lt;/strong&gt; tabel deviasi item + daftar issue prioritas&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;istilah-kekinian-yang-relevan-tanpa-sekadar-jargon-&quot;&gt;Istilah kekinian yang relevan (tanpa sekadar jargon)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single Source of Truth:&lt;/strong&gt; 1 definisi data yang disepakati&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data contract:&lt;/strong&gt; API schema sebagai kontrak lintas tim&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability:&lt;/strong&gt; tracing + log correlation (biar cepat cari bottleneck)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational analytics:&lt;/strong&gt; analitik yang langsung memicu aksi&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-faq-yang-biasanya-ditanyakan-di-dunia-nyata-&quot;&gt;7. FAQ (Yang Biasanya Ditanyakan di Dunia Nyata)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1) Apakah dashboard harus dibuat dengan React/Next.js?&lt;/strong&gt;&lt;br&gt;
Tidak wajib. Kamu bisa pakai BI tools (Metabase/Superset). Next.js cocok kalau kamu butuh kontrol penuh atas workflow, auth, dan UX.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) Kalau RAB masih Excel, apa langkah paling aman?&lt;/strong&gt;&lt;br&gt;
Mulai dari import terstruktur ke &lt;code&gt;budget_items&lt;/code&gt;, lalu simpan versi. Jangan overwrite baseline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) Data lapangan sering kosong. Apa yang dilakukan?&lt;/strong&gt;&lt;br&gt;
Mulai dari &lt;em&gt;minimum viable reporting&lt;/em&gt; (5–7 field wajib) + tampilkan field kosong sebagai &lt;em&gt;alert&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4) Apa metrik yang paling cepat memberi dampak?&lt;/strong&gt;&lt;br&gt;
Deviasi biaya vs progres per item pekerjaan—biasanya paling cepat “membuka mata”.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5) Bagaimana menghindari dashboard jadi “tempat debat”?&lt;/strong&gt;&lt;br&gt;
Tegaskan definisi metrik, lock baseline, dan simpan audit trail. Data yang bisa diaudit = diskusi yang lebih sehat.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;dashboard-yang-menjadi-alat-keputusan&quot;&gt;Dashboard yang Menjadi Alat Keputusan&lt;/h3&gt;
&lt;p&gt;Sebagai penutup, ada prinsip yang relevan untuk budaya engineering yang berujung pada kualitas sistem: “You build it, you run it.” — sering dikaitkan dengan &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Werner_Vogels&quot;&gt;Werner Vogels&lt;/a&gt;, CTO Amazon yang dikenal luas di ranah &lt;em&gt;distributed systems&lt;/em&gt; dan &lt;em&gt;cloud architecture&lt;/em&gt;. Artinya: “Kamu yang membangun, kamu juga yang menjalankan.” Dalam konteks dashboard, ini mengingatkan kita bahwa developer perlu peduli pada kualitas data, performa API, dan dampaknya ke operasional—karena feedback loop terbaik lahir dari sistem yang benar-benar dipakai.&lt;/p&gt;
&lt;p&gt;Kalau kamu sedang mengerjakan proyek industri/komersial dan ingin menautkan praktik digital (dashboard, MEP, hingga integrasi IT networking) ke workflow lapangan, kamu bisa melihat konteks layanan dan portofolio kami di &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.kontraktorkarawang.co.id/&quot;&gt;kontraktor Karawang&lt;/a&gt;—sebagai referensi dunia nyata bagaimana &lt;em&gt;dashboard manajemen proyek konstruksi&lt;/em&gt; akhirnya “turun ke tanah” dan dipakai harian.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;@context&quot;: &quot;https://schema.org&quot;,
  &quot;@graph&quot;: [
    {
      &quot;@type&quot;: &quot;Article&quot;,
      &quot;headline&quot;: &quot;Merancang Dashboard Manajemen Proyek Konstruksi: Progress, RAB, dan Issue Tracking Berbasis Web&quot;,
      &quot;description&quot;: &quot;Panduan praktis merancang dashboard manajemen proyek konstruksi yang menggabungkan progres, RAB vs aktual, dan issue tracking berbasis web—dengan fokus pada data contract, agregasi KPI, dan arsitektur MVP.&quot;,
      &quot;about&quot;: [
        &quot;Project Management&quot;,
        &quot;Construction Technology&quot;,
        &quot;Web Development&quot;,
        &quot;Observability&quot;
      ],
      &quot;keywords&quot;: [
        &quot;dashboard manajemen proyek konstruksi&quot;,
        &quot;progress proyek&quot;,
        &quot;RAB vs aktual&quot;,
        &quot;issue tracking&quot;,
        &quot;Next.js&quot;,
        &quot;TypeScript&quot;,
        &quot;PostgreSQL&quot;
      ],
      &quot;inLanguage&quot;: &quot;id-ID&quot;,
      &quot;author&quot;: {
        &quot;@type&quot;: &quot;Organization&quot;,
        &quot;name&quot;: &quot;PT. Nikifour&quot;
      },
      &quot;publisher&quot;: {
        &quot;@type&quot;: &quot;Organization&quot;,
        &quot;name&quot;: &quot;CoderLegion&quot;
      },
      &quot;mainEntityOfPage&quot;: {
        &quot;@type&quot;: &quot;WebPage&quot;,
        &quot;@id&quot;: &quot;https://coderlegion.com/&quot;
      }
    },
    {
      &quot;@type&quot;: &quot;HowTo&quot;,
      &quot;name&quot;: &quot;Merancang dashboard manajemen proyek konstruksi yang data-ready&quot;,
      &quot;description&quot;: &quot;Langkah demi langkah untuk merancang dashboard manajemen proyek konstruksi: menyepakati KPI, menyiapkan input harian, membangun API kontrak stabil, membuat endpoint agregasi, dan menerapkan audit trail.&quot;,
      &quot;totalTime&quot;: &quot;PT6H&quot;,
      &quot;supply&quot;: [
        { &quot;@type&quot;: &quot;HowToSupply&quot;, &quot;name&quot;: &quot;Akun hosting / server&quot; },
        { &quot;@type&quot;: &quot;HowToSupply&quot;, &quot;name&quot;: &quot;Database PostgreSQL&quot; }
      ],
      &quot;tool&quot;: [
        { &quot;@type&quot;: &quot;HowToTool&quot;, &quot;name&quot;: &quot;Next.js&quot; },
        { &quot;@type&quot;: &quot;HowToTool&quot;, &quot;name&quot;: &quot;TypeScript&quot; },
        { &quot;@type&quot;: &quot;HowToTool&quot;, &quot;name&quot;: &quot;PostgreSQL&quot; }
      ],
      &quot;step&quot;: [
        {
          &quot;@type&quot;: &quot;HowToStep&quot;,
          &quot;name&quot;: &quot;Sepakati metrik (KPI) proyek&quot;,
          &quot;text&quot;: &quot;Definisikan progress (volume/bobot/EVM sederhana), definisikan biaya (cash out vs commitment), dan definisikan prioritas issue (severity + SLA).&quot;
        },
        {
          &quot;@type&quot;: &quot;HowToStep&quot;,
          &quot;name&quot;: &quot;Buat input harian yang mobile-friendly&quot;,
          &quot;text&quot;: &quot;Gunakan form ringkas: tanggal, shift, cuaca, item + volume + bukti, resource, kendala + kategori.&quot;
        },
        {
          &quot;@type&quot;: &quot;HowToStep&quot;,
          &quot;name&quot;: &quot;Bangun API kontrak yang stabil&quot;,
          &quot;text&quot;: &quot;Terapkan validasi ketat, idempotency key untuk submit ulang, dan format error konsisten.&quot;
        },
        {
          &quot;@type&quot;: &quot;HowToStep&quot;,
          &quot;name&quot;: &quot;Buat endpoint agregasi dashboard&quot;,
          &quot;text&quot;: &quot;Sediakan endpoint yang mengembalikan KPI, data chart, alerts deviasi, dan issue SLA breach dalam format siap render.&quot;
        },
        {
          &quot;@type&quot;: &quot;HowToStep&quot;,
          &quot;name&quot;: &quot;Aktifkan audit trail&quot;,
          &quot;text&quot;: &quot;Simpan created_by/created_at/source, histori revisi laporan, dan versioning RAB untuk kebutuhan audit.&quot;
        }
      ]
    },
    {
      &quot;@type&quot;: &quot;FAQPage&quot;,
      &quot;mainEntity&quot;: [
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;Apakah dashboard harus dibuat dengan React/Next.js?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;Tidak wajib. Kamu bisa memakai BI tools (Metabase/Superset). Next.js cocok jika butuh kontrol penuh atas workflow, auth, dan UX.&quot;
          }
        },
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;Kalau RAB masih Excel, apa langkah paling aman?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;Mulai dari import terstruktur ke budget_items dan simpan versi. Jangan overwrite baseline agar audit tetap aman.&quot;
          }
        },
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;Data lapangan sering kosong. Apa yang dilakukan?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;Mulai dari minimum viable reporting (5–7 field wajib) dan tampilkan field kosong sebagai alert agar disiplin input terbentuk.&quot;
          }
        },
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;Apa metrik yang paling cepat memberi dampak?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;Deviasi biaya vs progres per item pekerjaan—biasanya paling cepat menunjukkan bottleneck dan potensi overrun.&quot;
          }
        }
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12133/merancang-dashboard-manajemen-proyek-konstruksi-progress-rab-issue-tracking-berbasis</guid>
<pubDate>Fri, 06 Mar 2026 17:00:00 +0000</pubDate>
</item>
<item>
<title>Patching the Gaps: A Production-Ready Guide to Google’s ADK with TypeScript</title>
<link>https://coderlegion.com/11368/patching-the-gaps-a-production-ready-guide-to-googles-adk-with-typescript</link>
<description>&lt;p&gt;Building production-grade AI agents is tricky. In a landscape shifting rapidly, betting your entire architecture on a single provider's raw SDK allows for quick starts but leads to brittle vendor lock-in. This is where Google’s Agent Development Kit (ADK) shines. It acts as a crucial abstraction layer, providing a standardized common interface for defining tools (similar to LiteLLM which Google ADK actually uses internally), events, and agent orchestration. This means you aren't locked solely into Gemini; you can design systems that utilize different models from various providers — even using them as fallbacks for one another — without rewriting your core business logic.&lt;/p&gt;
&lt;p&gt;However, if you’ve tried taking the ADK into production with TypeScript, you’ve likely hit a significant reality gap. While the Python documentation is comprehensive, the TypeScript side is remarkably thin, steering devs toward &quot;ADK Devtools&quot; which, while fine for local prototyping, simply don't cut it for a hardened production stack.&lt;/p&gt;
&lt;p&gt;Before we fix that, let's briefly strip away the jargon and define how the ADK actually works under the hood. It relies on three core concepts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Runner (Event Loop):&lt;/strong&gt; Think of this as the engine room. It’s an asynchronous loop that takes an incoming signal (an event), processes it through your agent’s logic (calling the LLM, executing tools), and determines the next step (updating state in the process).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State:&lt;/strong&gt; This is the &quot;short-term memory&quot; of the active conversation. It’s mutable data representing where the user is &lt;em&gt;right now&lt;/em&gt; in a flow (e.g., &lt;code&gt;{ step: 'collecting_address', attempt: 2 }&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;/strong&gt; The environment the runner operates in. This is the static stuff the agent needs to do its job but doesn't change turn-by-turn. Think database connections, user IDs, or API keys.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Here is the crucial part for production:&lt;/strong&gt; Events in the ADK aren't just passive text messages. They carry their own state payload representing a &quot;delta&quot; or in simple layman terms, change. The Runner uses this specific event state to update the overall session state before beginning the next turn (handled by the session service).&lt;/p&gt;
&lt;p&gt;You can read more about the Google ADK architecture here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://google.github.io/adk-docs/get-started/about/&quot;&gt;https://google.github.io/adk-docs/get-started/about/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In a real-world environment, you can't let that critical state live in volatile, in-memory storage. You need true persistence so agents retain context across server restarts and container scaling. Furthermore, if you’re running a modern TypeScript stack with ESM, the currently shipped &lt;code&gt;@google/adk&lt;/code&gt; package suffers from dependency conflicts that break standard builds.&lt;/p&gt;
&lt;p&gt;Today, we’re going to fix this. We'll walk through patching the ADK to bridge the ESM/CommonJS gap and implementing custom PostgreSQL-backed session and memory services to ensure that state is saved durably. It’s a deep dive, but essential for anyone moving beyond the &quot;Hello World&quot; phase.&lt;/p&gt;
&lt;p&gt;Here's the GitHub repo with complete code examples you can refer from: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TylerMutai/google-adk-tutorial.git&quot;&gt;tutorial repository&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-current-limitations&quot;&gt;The Current Limitations&lt;/h4&gt;
&lt;p&gt;For production TypeScript applications, the current ADK falls short in three key areas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Lack of Persistence:&lt;/strong&gt; The provided examples use in-memory services. Server restarts mean lost user context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESM/CJS Conflicts:&lt;/strong&gt; The published package's CommonJS build incorrectly imports the ESM-only &lt;code&gt;lodash-es&lt;/code&gt; package, rather than standard &lt;code&gt;lodash&lt;/code&gt;, breaking many build pipelines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing Infrastructure:&lt;/strong&gt; Unlike the Python SDK, the TypeScript version lacks built-in persistent session services (like VertexAI) out of the box.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We will resolve these issues using &lt;strong&gt;NestJS&lt;/strong&gt;, &lt;strong&gt;TypeORM&lt;/strong&gt; (PostgreSQL), and &lt;strong&gt;pnpm package manager&lt;/strong&gt;  for patching.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hands-on-building-a-persistent-adk-agent&quot;&gt;Hands-On: Building a Persistent ADK Agent&lt;/h3&gt;
&lt;p&gt;We are going to implement a &lt;code&gt;CloudSqlSessionService&lt;/code&gt; and a &lt;code&gt;CloudSqlMemoryService&lt;/code&gt;. This ensures your agent's state and long-term memories live safely in your Postgres database rather than ephemeral RAM.&lt;/p&gt;
&lt;h4 id=&quot;1-patching-google-adk&quot;&gt;1. Patching @google/adk&lt;/h4&gt;
&lt;p&gt;The first step is stabilizing the dependency tree. As mentioned, the &lt;code&gt;@google/adk&lt;/code&gt; package currently ships a CommonJS build that incorrectly imports &lt;code&gt;lodash-es&lt;/code&gt;. We need to patch this to get it running in a modern environment.&lt;/p&gt;
&lt;p&gt;Ensure you have &lt;a rel=&quot;nofollow&quot; href=&quot;https://pnpm.io/installation&quot;&gt;pnpm installed&lt;/a&gt;, then run (I assume here that you already have this dependency included in your project):&lt;br&gt;
&lt;code&gt;pnpm patch @google/adk&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open the temporary directory pnpm provides (it'll be printed out in your terminal) and make two changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In &lt;code&gt;package.json&lt;/code&gt;, add the standard &lt;code&gt;lodash&lt;/code&gt; (CommonJS) to dependencies, matching the version of the existing &lt;code&gt;lodash-es&lt;/code&gt; entry.&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;dist/cjs/index.js&lt;/code&gt;, do a find-and-replace: change all instances of &lt;code&gt;'lodash-es'&lt;/code&gt; to &lt;code&gt;'lodash'&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's my package.json example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;dependencies&quot;: {
    &quot;@google/genai&quot;: &quot;^1.37.0&quot;,
    &quot;@modelcontextprotocol/sdk&quot;: &quot;^1.24.0&quot;,
    &quot;google-auth-library&quot;: &quot;^10.3.0&quot;,
    &quot;lodash-es&quot;: &quot;^4.17.22&quot;,
    &quot;lodash&quot;: &quot;^4.17.22&quot;, &amp;lt;----- This change here. Also ensure version numbers match for lodash-es (above) and lodash.
    &quot;zod&quot;: &quot;3.25.76&quot;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply the fix:&lt;br&gt;
&lt;code&gt;pnpm patch-commit &amp;lt;path-to-temp-dir&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You can find the already-patched version in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TylerMutai/google-adk-tutorial.git&quot;&gt;tutorial repository&lt;/a&gt; if you want to skip this step.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;2-the-webpack-shim-for-nx-webpack-users-&quot;&gt;2. The Webpack Shim (For NX/Webpack users)&lt;/h4&gt;
&lt;p&gt;If you are using Webpack (or NX), you need to configure it to handle mixed module types. Since your project is likely ESM, but the patched ADK package is CJS, your &lt;code&gt;webpack.config.cjs&lt;/code&gt; (you should have a &lt;code&gt;webpack.config.js&lt;/code&gt; file if your project is commonjs) needs explicit handling for output formatting to ensure Node runs it correctly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Key configuration: Save output as .cjs to run as a CommonJS module while maintaining TS paths (if your project is esm), otherwise, use .js
config.output = {
  ...config.output,
  // ensure the output is treated as commonjs
  filename: '[name].cjs',
  chunkFilename: '[name].chunk.cjs',
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-implementing-persistent-sessions&quot;&gt;3. Implementing Persistent Sessions&lt;/h4&gt;
&lt;p&gt;Before we go to the actual meat of the implementation, I'll briefly explain the database entities and how they map to Google ADK types (feel free to skip this section if this is of no interest to you).&lt;/p&gt;
&lt;h5 id=&quot;the-persistence-schema-mapping-adk-to-postgresql&quot;&gt;The Persistence Schema: Mapping ADK to PostgreSQL&lt;/h5&gt;
&lt;p&gt;You'll need a database schema that mirrors the ADK's internal logic. Using &lt;strong&gt;TypeORM&lt;/strong&gt;, we define three primary entities: &lt;strong&gt;Sessions&lt;/strong&gt;, &lt;strong&gt;Events&lt;/strong&gt;, and &lt;strong&gt;Memories&lt;/strong&gt;.&lt;/p&gt;
&lt;h5 id=&quot;the-session-entity&quot;&gt;The Session Entity&lt;/h5&gt;
&lt;p&gt;The &lt;code&gt;SessionEntity&lt;/code&gt; acts as the root. It tracks the overall &lt;code&gt;state&lt;/code&gt; (the short-term memory) and metadata like &lt;code&gt;appName&lt;/code&gt; and &lt;code&gt;userId&lt;/code&gt;. This ensures that even if your server instances cycle, the agent knows exactly where it left off with a specific user.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { BaseEntity } from '@app/common/lib/entities/base.entity.mjs';
import { Column, Entity, OneToMany, Index } from 'typeorm';
import { EventEntity } from './event.entity.mjs';

@Entity('adk_sessions')
export class SessionEntity extends BaseEntity {
  @Column({ type: 'varchar', length: 100 })
  @Index({ unique: true })
  sessionId: string; // The ADK session identifier (e.g. phone_YYYY-MM-DD)

  @Column({ type: 'varchar', length: 255 })
  appName: string;

  @Column({ type: 'varchar', length: 255 })
  userId: string; // The phone number identifier

  @Column({ type: 'jsonb', default: {} })
  state: Record&amp;lt;string, unknown&amp;gt;;

  @Column({ type: 'bigint' })
  lastUpdateTime: number;

  @Column({ type: 'boolean', default: false })
  isSummarized: boolean; // To avoid re-summarizing already summarized sessions.

  @OneToMany(() =&amp;gt; EventEntity, (event: EventEntity) =&amp;gt; event.session, { cascade: true })
  events: EventEntity[];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;the-event-entity&quot;&gt;The Event Entity&lt;/h5&gt;
&lt;p&gt;Every interaction, whether it’s a user message, an agent response, or a tool execution, is an event. Notice the &lt;code&gt;actions&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt; columns are &lt;code&gt;jsonb&lt;/code&gt;. This is because ADK events are complex objects; storing them as JSON allows us to preserve the full payload without creating dozens of narrow columns.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Column, Entity, JoinColumn, ManyToOne, type Relation } from 'typeorm';
import { SessionEntity } from './session.entity.mjs';
import { BaseEntity } from '@app/common/lib/entities/base.entity.mjs';

@Entity('adk_events')
export class EventEntity extends BaseEntity {
  @Column({ type: 'varchar', length: 150, unique: true })
  eventId: string; // Global Event ID provided by ADK

  @ManyToOne(() =&amp;gt; SessionEntity, (session) =&amp;gt; session.events, { onDelete: 'CASCADE' })
  @JoinColumn()
  session: Relation&amp;lt;SessionEntity&amp;gt;;

  @Column({ type: 'jsonb' })
  actions: Record&amp;lt;string, unknown&amp;gt;; // Stores the full ADK Event object

  @Column({ type: 'varchar', length: 255 })
  invocationId: string;

  /**
   * Represents the entity that generated the event.
   * &quot;user&quot; - for input from the human user
   * &quot;Agent Name&quot; - (e.g. &quot;General Agent&quot;) for output from a specific AI agent
   */
  @Column({ type: 'varchar', length: 255, nullable: true })
  author: string;

  @Column({ type: 'bigint' })
  timestamp: number;

  @Column({ type: 'varchar', length: 255, nullable: true })
  branch: string;

  @Column({ type: 'jsonb' })
  content: Record&amp;lt;string, unknown&amp;gt;; // Stores the full ADK Event object
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;the-memory-entity&quot;&gt;The Memory Entity&lt;/h5&gt;
&lt;p&gt;Finally, the &lt;code&gt;MemoryEntity&lt;/code&gt; handles long-term persistence. The key column here is &lt;code&gt;embedding&lt;/code&gt;, which uses the &lt;code&gt;vector&lt;/code&gt; type. This is what allows your agent to perform semantic searches over thousands of past interactions to find relevant historical context.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Column, Entity, Index } from 'typeorm';
import type { Content } from '@google/genai';
import { BaseEntity } from '@app/common/lib/entities/base.entity.mjs';

@Entity('adk_memories')
export class MemoryEntity extends BaseEntity {
  @Column({ type: 'varchar', length: 255 })
  @Index()
  userId: string;

  @Column({ type: 'varchar', length: 255 })
  @Index()
  appName: string;

  @Column({ type: 'jsonb' })
  content: Content;

  @Column({ type: 'varchar', length: 255, nullable: true })
  author: string;

  @Column({ type: 'bigint' })
  timestamp: number;

  @Column({ type: 'vector', length: 768, nullable: true })
  embedding: number[];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;actual-implementation-the-services-layer&quot;&gt;Actual Implementation: The Services Layer&lt;/h5&gt;
&lt;p&gt;We need a custom &lt;code&gt;SessionService&lt;/code&gt; that persists state to PostgreSQL via TypeORM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Critical Implementation Detail:&lt;/strong&gt; When overriding the &lt;code&gt;appendEvent&lt;/code&gt; method in your custom service, you &lt;strong&gt;must&lt;/strong&gt; call &lt;code&gt;super.appendEvent({ session, event })&lt;/code&gt; first.&lt;br&gt;
This call registers the incoming event with the ADK framework's current in-memory execution context for the active &quot;turn.&quot; If you omit this, the framework won't recognize that an event occurred, even if you successfully save it to your database.&lt;/p&gt;
&lt;p&gt;Here's a code snippet of my implementation (you can find the full code example in the project repo):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class CloudSqlSessionService extends BaseSessionService {
  override async appendEvent({ session, event }: AppendEventRequest): Promise&amp;lt;Event&amp;gt; {
    // 1. Essential: Tell the ADK framework something happened in this turn
    await super.appendEvent({ session, event });

    // 2. Fetch current session state from DB
    const sessionEntity = await this.sessionsRepository.findOne({
      where: { sessionId: session.id },
    });

    // 3. Create event entity
    const eventEntity = this.eventsRepository.create({
      /* ... mapping logic ... */
    });

    await this.eventsRepository.save(eventEntity);
    
    // 4. Persist the updated state delta to Postgres
    await this.sessionsRepository.update(sessionEntity.id, {
      state: updatedState,
      lastUpdateTime: Date.now(),
    });

    return event;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-long-term-memory-vector-search-&quot;&gt;4. Long-Term Memory (Vector Search)&lt;/h4&gt;
&lt;p&gt;For long-term recall across different sessions, we implement a &lt;code&gt;CloudSqlMemoryService&lt;/code&gt;. This utilizes &lt;strong&gt;pgvector&lt;/strong&gt; within PostgreSQL for similarity search and Gemini’s &lt;code&gt;text-embedding-004&lt;/code&gt; model for generating embeddings.&lt;br&gt;
&lt;strong&gt;Important&lt;/strong&gt;: You MUST enable the pgvector extension in postgres. Refer to this link on how to install this extension: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/pgvector/pgvector&quot;&gt;installation instructions&lt;/a&gt;, then enable it by running this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE EXTENSION IF NOT EXISTS vector;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's the code example for saving and searching your long term memory (again, the full code example is available in the project repo):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class CloudSqlSessionService extends BaseSessionService {
  async addSessionToMemory(session: Session): Promise&amp;lt;void&amp;gt; {
    console.info(`------------- Adding session ${session.id} events (total events: ${session.events.length}) to memory for user ${session.userId} -------------`);

    for (const event of session.events) {
      console.log('event: ', event);
      console.log('event.content: ', event.content);
      const textContent = stringifyContent(event).trim();
      console.log('textContent: ', textContent, '\n\n');
      if (!textContent) {
        continue;
      }

      try {
        const embedding = await this.generateEmbedding(textContent);

        const memory = this.memoryRepository.create({
          userId: session.userId,
          appName: session.appName,
          content: event.content,
          author: event.author || 'unknown',
          timestamp: event.timestamp,
          embedding: embedding,
        });

        await this.memoryRepository.save(memory);
      } catch (e) {
        console.error(`Failed to generate embedding for event in session ${session.id}: ${e.message}`, e);
      }
    }
  }

  /**
   * Searches for relevant memories using vector similarity.
   */
  async searchMemory(request: SearchMemoryRequest): Promise&amp;lt;SearchMemoryResponse&amp;gt; {
    const { appName, userId, query } = request;

    try {
      const queryEmbedding = await this.generateEmbedding(query);
      const vectorStr = `[${queryEmbedding.join(',')}]`;

      // Use pgvector's cosine distance operator &amp;lt;=&amp;gt;
      const results = await this.dataSource.query(
        `SELECT id, content, author, timestamp
         FROM adk_memories
         WHERE &quot;userId&quot; = $1
           AND &quot;appName&quot; = $2
         ORDER BY embedding &amp;lt;=&amp;gt; $3
           LIMIT 5`,
        [userId, appName, vectorStr],
      );

      const memories: MemoryEntry[] = results.map(r =&amp;gt; ({
        content: r.content,
        author: r.author,
        timestamp: r.timestamp,
      }));

      return { memories };
    } catch (error) {
      console.error(`Failed to search memory: ${error.message}`, error);
      return { memories: [] };
    }
  }

  /**
   * Generates an embedding for the given text.
   */
  private async generateEmbedding(text: string): Promise&amp;lt;number[]&amp;gt; {
    const response = await this._genAI.models.embedContent({
      model: 'text-embedding-004',
      contents: [text],
    });
    if (!response.embeddings || response.embeddings.length === 0) {
      throw new Error('No embeddings returned from API');
    }
    return response.embeddings[0].values!;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-giving-the-agent-a-memory-tool&quot;&gt;5) Giving the Agent a &quot;Memory&quot; Tool&lt;/h4&gt;
&lt;p&gt;With your &lt;code&gt;CloudSqlMemoryService&lt;/code&gt; in place, you need a way for the agent to actually trigger a search. In the ADK, this is done by defining a &lt;code&gt;FunctionTool&lt;/code&gt;. This tool acts as the bridge between the LLM’s reasoning and your vector database.&lt;/p&gt;
&lt;p&gt;When the agent realizes it lacks specific historical context (e.g., &quot;What did we discuss about the project architecture last month?&quot;), it will call this &lt;code&gt;load_memory&lt;/code&gt; tool.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { FunctionTool } from '@google/adk';
import { z } from 'zod';

/**
 * A built-in tool that allows agents to load relevant long-term memories.
 */
const loadMemoryTool = new FunctionTool({
  name: 'load_memory',
  description: 'Loads relevant long-term memories for the current user based on a query.',
  parameters: z.object({
    query: z.string().describe('The search query for memory.'),
  }),
  execute: async ({ query }, toolContext) =&amp;gt; {
    if (!toolContext) {
      throw new Error('Tool context is missing');
    }
    // The ADK passes the active MemoryService via toolContext
    return await toolContext.searchMemory(query);
  },
});

export default loadMemoryTool;

&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;why-toolcontext-is-key&quot;&gt;Why &lt;code&gt;toolContext&lt;/code&gt; is Key&lt;/h5&gt;
&lt;p&gt;Notice that we don't import the memory service directly into the tool. Instead, we pull it from the &lt;code&gt;toolContext&lt;/code&gt;. This is an architectural win: it keeps your tools decoupled from specific database implementations and ensures that the search is automatically scoped to the correct &lt;code&gt;userId&lt;/code&gt; and &lt;code&gt;appName&lt;/code&gt; currently active in the session.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;6-orchestrating-the-agent-the-general-agent-factory-&quot;&gt;6) Orchestrating the Agent (The &quot;General Agent&quot; Factory)&lt;/h4&gt;
&lt;p&gt;Now that we have our persistent services and our &lt;code&gt;load_memory&lt;/code&gt; tool, we need to wire them into the agent. In a production NestJS or Express environment, you’ll often want to pass external dependencies (like database connections or configurations) into your agent definition.&lt;/p&gt;
&lt;p&gt;A clean way to do this is by using a &lt;strong&gt;factory function&lt;/strong&gt;. This allows you to inject your &lt;code&gt;MainContext&lt;/code&gt; directly into the agent and its sub-agents.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { LlmAgent } from '@google/adk';
import loadMemoryTool from '../load-memory.tool.mjs';

// Factory function to inject context and dependencies
export default function generalAgent(context: MainContext) {
  return new LlmAgent({
    name: 'general_agent',
    model: 'gemini-1.5-pro', // Use your preferred model constant
    instruction: `
  **Identity:**
  You are SkyCast, a helpful weather assistant. 
  **Current Date and Time:** {current_datetime}
  
  **Long-term Memory:** You can access the user's favorite locations and past queries by calling the \`load_memory\` tool.
  
  **User profile:** {user:profile?} 
  Use this to identify the user's home city or preferred units.

  **Core Responsibilities:**
  1. **Triage &amp;amp; Routing:** Route users to specific sub-agents (user, forecast, or alerts).
  2. **Memory Retrieval:** Proactively check memory for saved preferences if the user is vague.
  `,
    tools: [loadMemoryTool],
    // Sub-agents receive the same context for consistent behavior
    subAgents: [userAgent(context), forecastAgent(context), alertsAgent(context)],
  });
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h5 id=&quot;why-this-structure-wins&quot;&gt;Why This Structure Wins&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Recursive Context:&lt;/strong&gt; By passing &lt;code&gt;context&lt;/code&gt; into &lt;code&gt;subAgents&lt;/code&gt;, every agent in the hierarchy shares the same understanding of the user's session and database connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Instructions:&lt;/strong&gt; Notice the placeholders like &lt;code&gt;{current_datetime}&lt;/code&gt; and &lt;code&gt;{user:profile?}&lt;/code&gt;. The ADK fills these in at runtime, giving the LLM immediate &quot;short-term&quot; context before it even decides to call the &quot;long-term&quot; &lt;code&gt;load_memory&lt;/code&gt; tool.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent Handoffs:&lt;/strong&gt; The &lt;code&gt;subAgents&lt;/code&gt; array defines the &quot;routing&quot; capabilities. If the user asks to register, the General Agent handles the handoff to the &lt;code&gt;user_agent&lt;/code&gt; automatically based on your routing rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architectural-benefits&quot;&gt;Architectural Benefits&lt;/h3&gt;
&lt;p&gt;By moving off the default in-memory implementations and using a factory pattern for orchestration, we gain several production advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stateless Application Tier:&lt;/strong&gt; Offloading state to PostgreSQL means your application containers can scale horizontally without losing track of a user's progress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session Durability:&lt;/strong&gt; User context survives deployments, crashes, or server restarts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex Multi-Agent Logic:&lt;/strong&gt; The factory pattern allows you to build a sophisticated &quot;brain&quot; that delegates specialized tasks (like weather alerts or billing) while maintaining a single source of truth for user memory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h4&gt;
&lt;p&gt;By patching the ADK's module issues, implementing database-backed services, and orchestrating your agents through a clean factory pattern, you move from a prototype to a production-ready system. The result is a robust backend that handles state correctly, regardless of infrastructure churn.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/11368/patching-the-gaps-a-production-ready-guide-to-googles-adk-with-typescript</guid>
<pubDate>Tue, 10 Feb 2026 11:02:52 +0000</pubDate>
</item>
<item>
<title>Is OpenAI really running ChatGPT on a single PostgreSQL instance?</title>
<link>https://coderlegion.com/10554/is-openai-really-running-chatgpt-on-a-single-postgresql-instance</link>
<description>&lt;p&gt;The headline of OpenAI’s recent article &lt;a rel=&quot;nofollow&quot; href=&quot;https://openai.com/index/scaling-postgresql/&quot;&gt;https://openai.com/index/scaling-postgresql/&lt;/a&gt; feels a bit clickbaity, &lt;br&gt;
If they truly used only one database instance, ChatGPT would’ve been dead on arrival.&lt;/p&gt;
&lt;p&gt;But the reality is far more impressive: they’re using the right tool for each layer of the persistent stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;They push PostgreSQL to its absolute limits, with a single primary writer, yes, but backed by nearly 50 read replicas across global regions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For write-heavy workloads, they’ve wisely migrated to sharded systems like Azure Cosmos DB.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They’ve added layers of resilience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;connection pooling with PgBouncer,&lt;/li&gt;
&lt;li&gt;query rate limiting,&lt;/li&gt;
&lt;li&gt;caching with lock leasing,&lt;/li&gt;
&lt;li&gt;cascading replication (in testing),&lt;/li&gt;
&lt;li&gt;strict schema-change policies.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not magic, it’s mature, thoughtful engineering at scale.&lt;/p&gt;
&lt;p&gt;If you work with databases, this post is absolutely worth reading.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10554/is-openai-really-running-chatgpt-on-a-single-postgresql-instance</guid>
<pubDate>Tue, 27 Jan 2026 09:05:05 +0000</pubDate>
</item>
<item>
<title>Why Updating Data in Postgres Is More Expensive Than You Think</title>
<link>https://coderlegion.com/10024/why-updating-data-in-postgres-is-more-expensive-than-you-think</link>
<description>&lt;h3 id=&quot;context&quot;&gt;Context&lt;/h3&gt;
&lt;p&gt;I was recently tasked with optimizing stockinsights.ai’s full text search system for one of our core features: keyword search over filings.&lt;/p&gt;
&lt;p&gt;Previously, this worked by storing entire documents as raw strings in MongoDB and scanning through them at query time to return surrounding snippets. As you can imagine, this approach became increasingly slow and inefficient as the number of filings kept growing.&lt;/p&gt;
&lt;p&gt;This clearly needed to be fixed.&lt;/p&gt;
&lt;p&gt;While looking at the existing system, I noticed that we already stored chunked text snippets in a separate Postgres instance as part of our embeddings tables for semantic search. Each chunk already existed as a row, paired with an embedding.&lt;/p&gt;
&lt;p&gt;It seemed obvious that we should reuse this same chunked data for keyword search as well.&lt;/p&gt;
&lt;p&gt;At a high level, the idea looked something like this:&lt;/p&gt;
&lt;p&gt;Keyword search across snippets&lt;br&gt;
→ return matching text chunks&lt;br&gt;
→ rank documents higher if more chunks match&lt;br&gt;
→ return one representative snippet per document&lt;/p&gt;
&lt;p&gt;All I needed to do, or so I thought, was add another column to store &lt;code&gt;tsvectors&lt;/code&gt; (Postgres’s internal data structure for full text search) and create a GIN index on top of it.&lt;/p&gt;
&lt;p&gt;How hard could it be?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The obvious solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Postgres has built-in full text search using tsvector and tsquery. The plan was straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Add a tsvector column to the embeddings table
Populate it from the existing text column
Create a GIN index
Query snippets directly instead of scanning raw documents
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: We could have normalized this into a separate table, but at a startup, we often accept some denormalization in favor of simpler systems.&lt;/p&gt;
&lt;p&gt;From an application point of view, this was almost trivial. No new data model, no new pipeline, just reuse what already existed.&lt;/p&gt;
&lt;p&gt;The mistake was assuming that a small logical change would translate into a small physical change inside the database.&lt;/p&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;h3 id=&quot;when-the-costs-showed-up&quot;&gt;When the costs showed up&lt;/h3&gt;
&lt;p&gt;**&lt;br&gt;
Initially, nothing failed. The migration just took much longer than expected.&lt;/p&gt;
&lt;p&gt;Each document type table had roughly 2 to 5 million rows. Updating even 10,000 rows was taking hours, and each iteration made the next one slower. What started as a small backfill quickly turned into something that felt unbounded in cost.&lt;/p&gt;
&lt;p&gt;Populating the new column dragged on, disk usage grew far beyond what the additional data alone could justify, and creating the GIN index noticeably impacted write throughput while it was running.&lt;/p&gt;
&lt;p&gt;More concerning was that queries themselves began to slow down. Reads that were previously fast started degrading as the table and indexes bloated, even though the query plans hadn’t meaningfully changed.&lt;/p&gt;
&lt;p&gt;At that point, it was clear that something deeper was going on.&lt;/p&gt;
&lt;h3 id=&quot;update-in-postgres-is-not-mutation&quot;&gt;UPDATE in Postgres is not mutation&lt;/h3&gt;
&lt;p&gt;Postgres stores tables as heap files. A heap file is an unordered collection of tuples, and tuples are immutable.&lt;/p&gt;
&lt;p&gt;When you run an UPDATE, Postgres does not modify a row in place. Instead, it:&lt;/p&gt;
&lt;p&gt;Inserts a new version of the row&lt;br&gt;
Marks the old version as no longer visible&lt;br&gt;
Leaves cleanup for later&lt;br&gt;
This design exists to support MVCC (multi-version concurrency control), which allows readers and writers to operate concurrently by keeping multiple versions of rows and resolving visibility at read time.&lt;/p&gt;
&lt;p&gt;In practice, this means UPDATE is not a mutation of an existing row, but the creation of a new version of that row.&lt;/p&gt;
&lt;h3 id=&quot;why-adding-a-column-rewrites-every-row&quot;&gt;Why adding a column rewrites every row&lt;/h3&gt;
&lt;p&gt;A Postgres row is stored as a single physical tuple containing all columns. What I learned here is that there is no column-level storage, and therefore no partial row update.&lt;/p&gt;
&lt;p&gt;When you populate a new column on an existing table, Postgres must:&lt;/p&gt;
&lt;p&gt;Copy every existing column value&lt;br&gt;
Append the new column value&lt;br&gt;
Write an entirely new tuple&lt;br&gt;
Even if every other column remains unchanged, the entire row is rewritten. On a large table, this effectively means rewriting the entire dataset, not just adding one column.&lt;/p&gt;
&lt;p&gt;Why text, tsvector, and embeddings make this worse&lt;br&gt;
Our table already stored text snippets and embeddings, and we were about to add tsvectors on top of that.&lt;/p&gt;
&lt;p&gt;A few things compound the cost here:&lt;/p&gt;
&lt;p&gt;Text columns are variable length. Large values often exceed the inline storage limit. When that happens, Postgres uses TOAST.&lt;br&gt;
TOAST stores oversized column values out of line and keeps pointers to them in the main tuple.&lt;/p&gt;
&lt;p&gt;Even when data is TOASTed, updating the row still requires rewriting the main tuple and, in some cases, the TOAST entries themselves.&lt;/p&gt;
&lt;p&gt;tsvector expands data. It is not just the original text. It stores lexemes (similar to tokens) along with positional metadata, and it is also typically stored via TOAST.&lt;/p&gt;
&lt;p&gt;Embeddings make rows wide. Embeddings are large binary float arrays stored alongside text and indexed separately. Combined with text and tsvector, each row becomes wide, and wide rows are expensive to rewrite.&lt;/p&gt;
&lt;p&gt;This combination pushes right up against the limits of what an immutable row store is comfortable handling.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Indexes amplify the cost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On top of this, the table already had indexes.&lt;/p&gt;
&lt;p&gt;Every new tuple version requires index maintenance. For each index, Postgres must:&lt;/p&gt;
&lt;p&gt;Insert new index entries&lt;br&gt;
Maintain index structure&lt;br&gt;
Write corresponding WAL records&lt;br&gt;
GIN indexes are especially heavy. A single row can generate many index entries, often one per token in the tsvector.&lt;/p&gt;
&lt;p&gt;While most indexes in Postgres are B-tree based, GIN indexes are inverted indexes.&lt;/p&gt;
&lt;p&gt;WAL (write-ahead log) is the sequential log used for crash recovery and replication. More index work means more WAL traffic, which directly affects disk usage, replication lag, and write latency.&lt;/p&gt;
&lt;p&gt;Indexes make reads fast, but they multiply write cost. That tradeoff is unavoidable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;An alternate approach: COPY, swap, drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While digging through Postgres blogs and discussions, I came across an alternative approach for modifying very large tables.&lt;/p&gt;
&lt;p&gt;Instead of updating the table in place, you:&lt;/p&gt;
&lt;p&gt;Create a new table with the desired schema&lt;br&gt;
Copy data into it&lt;br&gt;
Recreate indexes&lt;br&gt;
Swap it with the old table&lt;br&gt;
Drop the old table&lt;br&gt;
The COPY-swap-drop approach.&lt;/p&gt;
&lt;p&gt;This has a few important properties:&lt;/p&gt;
&lt;p&gt;Each row is written exactly once&lt;br&gt;
No dead tuples are created&lt;br&gt;
Indexes are built cleanly&lt;br&gt;
There is no incremental index maintenance during updates&lt;br&gt;
By contrast, UPDATE creates deferred work. Dead tuples accumulate and must be cleaned up later, and indexes are updated repeatedly for each new row version.&lt;/p&gt;
&lt;p&gt;In an immutable storage model, rewriting data cleanly can often be cheaper than mutating it incrementally.&lt;/p&gt;
&lt;h3 id=&quot;final-design&quot;&gt;Final design&lt;/h3&gt;
&lt;p&gt;One additional factor that helped was simply scaling the RDS instance. More memory meant more rows could be processed efficiently during the rebuild, which significantly reduced runtime.&lt;/p&gt;
&lt;p&gt;The final workflow looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Upgrade the instance
Use COPY-swap-drop to rebuild the table
Recreate indexes
This approach ended up being far faster and more predictable than trying to UPDATE rows in place.
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10024/why-updating-data-in-postgres-is-more-expensive-than-you-think</guid>
<pubDate>Tue, 20 Jan 2026 03:30:41 +0000</pubDate>
</item>
<item>
<title>Oracle migrations reveal hidden technical debt. Here's what developers need to know before moving.</title>
<link>https://coderlegion.com/6394/oracle-migrations-reveal-hidden-technical-debt-heres-what-developers-need-before-moving</link>
<description>&lt;h2 id=&quot;the-real-cost-of-oracle-technical-debt-what-developers-learn-during-migration&quot;&gt;The Real Cost of Oracle Technical Debt: What Developers Learn During Migration&lt;/h2&gt;
&lt;p&gt;Oracle databases have been around for 40 years. That's a lot of time to accumulate technical debt.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/spencerwkimball/&quot;&gt;Spencer Kimball&lt;/a&gt;, CEO of &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cockroachlabs.com/&quot;&gt;Cockroach Labs&lt;/a&gt;, spends his days helping Fortune 100 companies migrate off Oracle. The technical challenges he sees are consistent: undocumented stored procedures, excessive use of legacy patterns, and architectural decisions made when networking was slow and security wasn't a priority.&lt;/p&gt;
&lt;p&gt;&quot;The people that wrote them are gone from our company,&quot; Kimball says of the legacy applications his customers want to migrate. &quot;We want help solving this.&quot;&lt;/p&gt;
&lt;h3 id=&quot;postgresql-is-eating-the-world&quot;&gt;PostgreSQL is Eating the World&lt;/h3&gt;
&lt;p&gt;The first thing to understand about modern database migrations is that developers don't want Oracle's dialect anymore. They want PostgreSQL.&lt;/p&gt;
&lt;p&gt;&quot;Postgres is actively eating the world,&quot; Kimball says. When Cockroach Labs started 11 years ago, PostgreSQL wasn't the default choice. Now it is.&lt;/p&gt;
&lt;p&gt;This shift shows up everywhere. AWS Aurora moved its center of gravity from MySQL to PostgreSQL. Even TiDB, which started as a MySQL engine, recently added PostgreSQL support. IBM's customers are asking for &quot;a Postgres database for the enterprise.&quot;&lt;/p&gt;
&lt;p&gt;The reason is straightforward: PostgreSQL is the standard for new development. If you're building something new, you're building it on Postgres.&lt;/p&gt;
&lt;h3 id=&quot;distributed-vs-monolithic-where-developers-get-surprised&quot;&gt;Distributed vs Monolithic: Where Developers Get Surprised&lt;/h3&gt;
&lt;p&gt;The migration from Oracle to a distributed database like CockroachDB involves more than syntax changes. It requires a shift in thinking.&lt;/p&gt;
&lt;p&gt;&quot;Databases have grown up over the last 50 years with this perspective that the database is a tightly coupled monolithic system,&quot; Kimball explains. Oracle was built for InfiniBand networks and specialized hardware. Those requirements don't match cloud reality.&lt;/p&gt;
&lt;p&gt;The most common surprise? Auto-incrementing IDs.&lt;/p&gt;
&lt;p&gt;In a monolithic database, generating sequential IDs is fast because everything runs on one machine. In a distributed system with consensus requirements, that pattern creates bottlenecks.&lt;/p&gt;
&lt;p&gt;The solution is UUID generation with node IDs embedded. These IDs are unique across the cluster, sort correctly, and create zero hot spots. It's a small change, but every developer on a migration team needs to understand it.&lt;/p&gt;
&lt;p&gt;&quot;Some of this is solved just by retraining developers,&quot; Kimball says. &quot;Some of it is done by tooling that will look through a schema and say, 'Here's an auto-incrementing ID. We recommend you replace it with UUID generation.'&quot;&lt;/p&gt;
&lt;h3 id=&quot;ai-is-unraveling-oracle-lock-in&quot;&gt;AI is Unraveling Oracle Lock-In&lt;/h3&gt;
&lt;p&gt;Stored procedures represent Oracle's most effective lock-in mechanism. They were a reasonable architectural choice when network latency was high and database security was limited. DBAs pushed application logic into the database where it could be controlled and protected.&lt;/p&gt;
&lt;p&gt;The result is mountains of undocumented code. Sometimes it's PL/SQL. Sometimes it's C routines with no unit tests and no documentation.&lt;/p&gt;
&lt;p&gt;AI tools are changing the migration economics. Kimball says AI can analyze legacy stored procedures, generate documentation, create unit tests, and suggest complete migrations to PL/pgSQL (PostgreSQL's procedural language).&lt;/p&gt;
&lt;p&gt;&quot;The sharpest decrease in costs is going to come from AI,&quot; he says. Where migrations used to require 100+ consultants over three years at a cost of $10 million, AI-assisted approaches can reduce that significantly.&lt;/p&gt;
&lt;p&gt;The tools from Cognition, Devin, and even Claude can handle much of the heavy lifting that previously required specialized Oracle knowledge.&lt;/p&gt;
&lt;h3 id=&quot;architecture-built-for-ai-scale&quot;&gt;Architecture Built for AI Scale&lt;/h3&gt;
&lt;p&gt;The architectural differences between distributed and monolithic databases matter more as AI workloads increase.&lt;/p&gt;
&lt;p&gt;&quot;You can't even get there without a distributed architecture,&quot; Kimball says about handling AI-scale traffic. The issue isn't just throughput. It's cost.&lt;/p&gt;
&lt;p&gt;Cockroach Labs routinely runs 300-node test clusters for weeks at a time. These clusters contain petabytes of data. Three weeks used to cost $500,000.&lt;/p&gt;
&lt;p&gt;Now they run these tests on spot instances and save 80%. &quot;Could you run a traditional database across spot instances?&quot; Kimball asks. &quot;You'd be down most of the time.&quot;&lt;/p&gt;
&lt;p&gt;A distributed database built for resilience can handle spot instances getting preempted. It replicates, rebalances, and keeps running through the chaos.&lt;/p&gt;
&lt;p&gt;The company is targeting a 10x cost improvement through a combination of optimizations: spot instances, multi-tenancy (packing multiple workloads on the same physical database), layered storage (S3, locally attached SSDs, EBS), and continuous performance optimization.&lt;/p&gt;
&lt;p&gt;They've already achieved 2x performance improvement on the same hardware this year.&lt;/p&gt;
&lt;h3 id=&quot;resilience-under-pressure&quot;&gt;Resilience Under Pressure&lt;/h3&gt;
&lt;p&gt;Performance benchmarks under ideal conditions don't tell you much about production reality. Cockroach Labs released a &quot;Performance under Adversity&quot; benchmark that measures how databases behave during chaos: pod failures, network partitions, disk stalls.&lt;/p&gt;
&lt;p&gt;They tested CockroachDB against Oracle GDD 23ai (the first version of Oracle that can support GDD in a Raft configuration). The results are documented in detail, and the benchmark methodology is open so anyone can reproduce it.&lt;/p&gt;
&lt;p&gt;CockroachDB encountered 56 errors resulting in 51 seconds of downtime across all chaos tests. Oracle GDD encountered 300 errors resulting in 369 seconds of downtime (over six minutes).&lt;/p&gt;
&lt;p&gt;That's 5x more resilient.&lt;/p&gt;
&lt;p&gt;The difference matters most during &quot;grey failures&quot;—those problems that don't immediately take the database offline but slowly degrade latency and breach SLAs.&lt;/p&gt;
&lt;h3 id=&quot;the-migration-path-forward&quot;&gt;The Migration Path Forward&lt;/h3&gt;
&lt;p&gt;If you're stuck on Oracle and planning a migration, here's what to focus on:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit your stored procedures.&lt;/strong&gt; Use AI tools to document what you have before you start planning the migration. You probably have more technical debt than you think.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Understand distributed architecture patterns.&lt;/strong&gt; Auto-incrementing IDs are just one example. Get your team trained on how distributed consensus works and where the architectural differences matter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test for resilience, not just performance.&lt;/strong&gt; Peak performance under ideal conditions matters less than consistent performance under realistic failure scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Factor in the full cost.&lt;/strong&gt; Don't just compare license fees. Include the cost of downtime, the complexity of running the database, and the total cost of ownership over time.&lt;/p&gt;
&lt;p&gt;The cloud forced many companies to reconsider Oracle. AI is accelerating that shift. If you're going to modernize anyway—and you probably are—make sure you understand the technical implications before you start.&lt;/p&gt;
</description>
<category>Data Engineering</category>
<guid isPermaLink="true">https://coderlegion.com/6394/oracle-migrations-reveal-hidden-technical-debt-heres-what-developers-need-before-moving</guid>
<pubDate>Mon, 13 Oct 2025 21:25:28 +0000</pubDate>
</item>
<item>
<title>PostgreSQL 18 Uncovered: Asynchronous I/O, UUIDv7, and Pain-Free Upgrades</title>
<link>https://coderlegion.com/5803/postgresql-18-uncovered-asynchronous-i-o-uuidv7-and-pain-free-upgrades</link>
<description>&lt;p&gt;As is tradition around this time of year, the global PostgreSQL community has surprised us with a new major version, and this time is no exception. On September 25th, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/&quot;&gt;PostgreSQL Global Development Group&lt;/a&gt; officially released &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/18/release-18.html&quot;&gt;PostgreSQL 18&lt;/a&gt;&lt;/strong&gt;. I'll tell you right now: it's one of the most exciting updates in recent years, especially if you're obsessed with performance and operational efficiency.&lt;/p&gt;
&lt;p&gt;In this article, we'll break down the official announcement to uncover the new features that will genuinely change the way we work—from developing custom software to deploying high-availability architectures. Forget minor tweaks; these are game-changing improvements. Let's dive in!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-key-new-features-in-postgresql-18&quot;&gt;The Key New Features in PostgreSQL 18&lt;/h3&gt;
&lt;p&gt;After analyzing the announcement, I've identified four areas that I consider revolutionary.&lt;/p&gt;
&lt;h4 id=&quot;the-headliner-asynchronous-i-o-aio-for-a-massive-performance-boost&quot;&gt;The Headliner: Asynchronous I/O (AIO) for a Massive Performance Boost&lt;/h4&gt;
&lt;p&gt;This is the crown jewel of PostgreSQL 18. Until now, Postgres relied on the operating system's &lt;code&gt;readahead&lt;/code&gt; mechanisms to speed up data retrieval, but this isn't always efficient.&lt;/p&gt;
&lt;p&gt;PostgreSQL 18 introduces a &lt;strong&gt;new Asynchronous I/O (AIO) subsystem&lt;/strong&gt;. Instead of requesting a block of data and waiting, it can now issue multiple I/O requests in parallel.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What does this mean?&lt;/strong&gt; For operations that read a lot of data from disk (like sequential scans, &lt;code&gt;VACUUM&lt;/code&gt;, etc.), the impact is huge. Community benchmarks have shown &lt;strong&gt;performance improvements of up to 3x&lt;/strong&gt; in certain scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do you use it?&lt;/strong&gt; It's controlled by the new &lt;code&gt;io_method&lt;/code&gt; parameter, which lets you choose between different implementations like &lt;code&gt;io_uring&lt;/code&gt; (on Linux) or stick with the classic synchronous behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; If you have a read-intensive (I/O-bound) workload, like analytics systems or applications with large datasets, you're going to see a night-and-day difference.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;upgrades-are-finally-pain-free&quot;&gt;Upgrades Are Finally Pain-Free&lt;/h4&gt;
&lt;p&gt;Anyone who has managed a production database knows the post-upgrade dread: queries run slow until &lt;code&gt;ANALYZE&lt;/code&gt; rebuilds the planner statistics. PostgreSQL 18 tackles this problem head-on.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Statistics Preservation:&lt;/strong&gt; Finally! It's now possible to &lt;strong&gt;keep planner statistics during a major version upgrade&lt;/strong&gt;. This means your database will perform at its optimal level almost immediately after upgrading, eliminating that painful performance degradation phase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster &lt;code&gt;pg_upgrade&lt;/code&gt;:&lt;/strong&gt; The upgrade utility itself has been optimized. It can now run checks in parallel (&lt;code&gt;--jobs&lt;/code&gt;) and has a new &lt;code&gt;--swap&lt;/code&gt; option that exchanges data directories instead of copying them, dramatically speeding up the process.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; The process of upgrading to a new major version will be much faster and less disruptive to your applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;developer-gems-you-ll-love&quot;&gt;Developer Gems You'll Love&lt;/h4&gt;
&lt;p&gt;This release is packed with features that make our lives easier and allow us to write more modern and efficient code.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Say Hello to &lt;code&gt;uuidv7()&lt;/code&gt;!:&lt;/strong&gt; A native function to generate v7 UUIDs has been added. Unlike random v4 UUIDs, v7 UUIDs are &lt;strong&gt;time-ordered&lt;/strong&gt;. Using them as primary keys drastically improves B-Tree index performance by reducing fragmentation and improving data locality. This is a fundamental change for high-insertion tables!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virtual Generated Columns:&lt;/strong&gt; Generated columns (&lt;code&gt;GENERATED COLUMNS&lt;/code&gt;) are now virtual by default. This means the &lt;strong&gt;value is computed at query time&lt;/strong&gt; instead of being stored on disk, saving space for values that aren't needed constantly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RETURNING&lt;/code&gt; with &lt;code&gt;OLD&lt;/code&gt; and &lt;code&gt;NEW&lt;/code&gt;:&lt;/strong&gt; DML statements (&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;MERGE&lt;/code&gt;) can now access both the old (&lt;code&gt;OLD&lt;/code&gt;) and new (&lt;code&gt;NEW&lt;/code&gt;) values directly in the &lt;code&gt;RETURNING&lt;/code&gt; clause. This is fantastic for creating audit logs or implementing change data capture logic in a single query.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; Native tools for modern design patterns that previously required more complex workarounds.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;authentication-security-and-important-changes-to-note&quot;&gt;Authentication, Security, and Important Changes to Note&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Native OAuth 2.0 Support:&lt;/strong&gt; This is one of the hidden gems of the release, especially for professional environments. In practice, it means you can now authenticate database users through external identity providers like Auth0, Okta, Microsoft Entra ID (Azure AD), or Google. Technically, this is enabled directly in the &lt;code&gt;pg_hba.conf&lt;/code&gt; file with a new &lt;code&gt;oauth&lt;/code&gt; authentication method. You configure key parameters like the &lt;code&gt;issuer&lt;/code&gt; (your identity provider's URL) and the &lt;code&gt;scope&lt;/code&gt; (the permissions the access token must have). The token validation itself is modular and delegated to specialized libraries, making the system highly flexible. The result is that instead of managing local passwords, you can delegate authentication to your central Single Sign-On (SSO) system. This not only radically simplifies user management but also elevates security by enabling centralized policies like multi-factor authentication (MFA).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The End of an Era: MD5 Deprecation:&lt;/strong&gt; Although secure authentication with SCRAM has been available for several versions, PostgreSQL 18 takes the final step: it &lt;strong&gt;officially deprecates the use of MD5 for passwords&lt;/strong&gt;. Support will be completely removed in a future release, so there are no more excuses. The community's message is clear: if you're still using MD5, migrating to SCRAM is an urgent and high-priority task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default Checksums:&lt;/strong&gt; New clusters created with &lt;code&gt;initdb&lt;/code&gt; will now have &lt;strong&gt;page checksums enabled by default&lt;/strong&gt;. This improves data integrity but is a crucial detail to consider when planning an upgrade from a cluster without them.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;my-critical-perspective-what-this-means-for-your-projects&quot;&gt;My Critical Perspective: What This Means for Your Projects&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Is Asynchronous I/O a silver bullet?&lt;/strong&gt; No. It's a powerful tool for I/O-bound workloads. If your bottleneck is the CPU, you won't see a magical improvement. But for deploying analytics applications or AI services that process large volumes of data, the impact will be enormous.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Real Operational Game-Changer:&lt;/strong&gt; While AIO gets the headlines, I believe &lt;strong&gt;preserving statistics on upgrades is the single most important feature for production systems&lt;/strong&gt;. It eliminates one of the biggest headaches and risks associated with an upgrade, reducing uncertainty and making it easier to keep your tech stack up to date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For Custom Software Development:&lt;/strong&gt; My recommendation is to start using &lt;strong&gt;&lt;code&gt;uuidv7()&lt;/code&gt; as the default primary key&lt;/strong&gt; on all new tables where it makes sense. It's an easy and free performance win. The improvements to &lt;code&gt;RETURNING&lt;/code&gt; will also greatly simplify audit logic in the backends I build.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Ace Up Your Sleeve for Professional Projects (OAuth 2.0):&lt;/strong&gt; Don't underestimate this feature. If you build custom software for businesses, as I do, offering integration with their SSO system (like Microsoft 365 or Google Workspace) is no longer a nightmare. It's an enterprise-grade feature that now comes standard, allowing you to sell more secure and better-integrated projects into your clients' ecosystems. It's a powerful differentiator.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Immediate Call to Action:&lt;/strong&gt; Audit your systems. If you're still using MD5 authentication, &lt;strong&gt;plan your migration to SCRAM now&lt;/strong&gt;. This isn't a suggestion; it's an imminent security requirement.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hands-on-test-postgresql-18-with-docker-and-examples&quot;&gt;Hands-On: Test PostgreSQL 18 with Docker and Examples&lt;/h3&gt;
&lt;p&gt;To understand why the deprecation of MD5 is such a big deal, there's nothing like seeing it in action. We'll use Docker to set up a lab with two parallel instances of PostgreSQL 18: one configured to use the old and now-obsolete &lt;strong&gt;MD5&lt;/strong&gt; method, and another using the modern and secure &lt;strong&gt;SCRAM&lt;/strong&gt;, which has been the recommended method for years. This way, you can see the security difference for yourself.&lt;/p&gt;
&lt;h4 id=&quot;requirements&quot;&gt;Requirements&lt;/h4&gt;
&lt;p&gt;To execute these examples you will need to install &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.docker.com/get-started/get-docker/&quot;&gt;Docker&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;step-1-set-up-the-comparison-environment&quot;&gt;Step 1: Set Up the Comparison Environment&lt;/h4&gt;
&lt;p&gt;Create a file named &lt;code&gt;docker-compose.yml&lt;/code&gt;. Notice that we're defining the two databases (&lt;code&gt;db_md5&lt;/code&gt; and &lt;code&gt;db_scram&lt;/code&gt;), the &lt;code&gt;adminer&lt;/code&gt; web interface, and an extra container named &lt;code&gt;pg_client&lt;/code&gt; that will act as our toolbox with &lt;code&gt;psql&lt;/code&gt; pre-installed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; (MD5 vs. SCRAM Comparison)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  db_md5:
    image: postgres:18
    container_name: postgres18_md5
    restart: always
    command: postgres -c password_encryption=md5 -c io_method=sync
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: testdb
    ports:
      - &quot;5432:5432&quot;
    volumes:
      - postgres_data_md5:/var/lib/postgresql/data

  db_scram:
    image: postgres:18
    container_name: postgres18_scram
    restart: always
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: testdb
    ports:
      - &quot;5433:5432&quot;
    volumes:
      - postgres_data_scram:/var/lib/postgresql/data

  pg_client:
    image: postgres:18
    container_name: pg_client
    # This command keeps the container running so we can exec into it
    command: tail -f /dev/null

  adminer:
    image: adminer
    container_name: postgres18_adminer
    restart: always
    ports:
      - &quot;8080:8080&quot;

volumes:
  postgres_data_md5:
  postgres_data_scram:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a terminal, run &lt;code&gt;docker-compose up -d&lt;/code&gt; or &lt;code&gt;docker compose up -d&lt;/code&gt;. Your entire environment is now ready, with no need to install anything else on your machine.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Note: The password for the &lt;code&gt;user&lt;/code&gt; in all connections that require it (psql, Adminer) is &lt;code&gt;password&lt;/code&gt;, as defined in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-2-run-the-examples-and-compare&quot;&gt;Step 2: Run the Examples and Compare&lt;/h4&gt;
&lt;p&gt;For the terminal examples, we'll use our &lt;code&gt;pg_client&lt;/code&gt; container.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to use the terminal:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open a shell session inside the client container with this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it pg_client bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once inside (you'll see a prompt like &lt;code&gt;root@...:/#&lt;/code&gt;), you can run &lt;code&gt;psql&lt;/code&gt; commands against the databases using their service names (&lt;code&gt;db_scram&lt;/code&gt; or &lt;code&gt;db_md5&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Connect to the SCRAM (secure) instance:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Terminal:&lt;/strong&gt; Inside the &lt;code&gt;pg_client&lt;/code&gt; container, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql -h db_scram -U user -d testdb
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adminer (&lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:8080/?pgsql=db_scram&amp;amp;username=user&amp;amp;db=testdb&quot;&gt;http://localhost:8080&lt;/a&gt;):&lt;/strong&gt; Server: &lt;code&gt;db_scram&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;![Login Adminer - db_scram][1]&lt;/p&gt;
&lt;h5 id=&quot;example-1-the-new-uuidv7-&quot;&gt;Example 1: The new &lt;code&gt;uuidv7()&lt;/code&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Compare the results of the two UUID types
SELECT uuidv4(), uuidv7();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the query a few times. You'll notice that &lt;code&gt;uuidv4()&lt;/code&gt; values are completely random, while &lt;code&gt;uuidv7()&lt;/code&gt; values start with a similar sequence that changes slightly. That initial part is the timestamp, which allows the index to sort them efficiently!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;testdb=# SELECT uuidv4(), uuidv7();
                uuidv4                |                uuidv7                
--------------------------------------+--------------------------------------
 a3c361d2-4aa2-49fe-84fd-247d249484e0 | 019999e7-70ce-7e41-ad73-97cd6e42f2a8
(1 row)

testdb=# SELECT uuidv4(), uuidv7();
                uuidv4                |                uuidv7                
--------------------------------------+--------------------------------------
 3c8115a7-8667-47e2-971c-b7ce61504572 | 019999e7-7713-75db-a7ff-e7c2ebbfb47f
(1 row)

testdb=# SELECT uuidv4(), uuidv7();
                uuidv4                |                uuidv7                
--------------------------------------+--------------------------------------
 1fc5216d-a874-418b-abcb-0fd389823abc | 019999e7-7c0f-7a0a-a364-6a3a536a0b6c
(1 row)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;example-2-virtual-generated-columns&quot;&gt;Example 2: Virtual Generated Columns&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE products (
    name TEXT,
    price_eur NUMERIC,
    price_usd NUMERIC GENERATED ALWAYS AS (price_eur * 1.07) VIRTUAL
);

INSERT INTO products (name, price_eur) VALUES ('Mechanical Keyboard', 80);

SELECT * FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;price_usd&lt;/code&gt; column doesn't take up disk space; it's calculated every time you query it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;testdb=# CREATE TABLE products (
    name TEXT,
    price_eur NUMERIC,
    price_usd NUMERIC GENERATED ALWAYS AS (price_eur * 1.07) VIRTUAL
);
CREATE TABLE
testdb=# INSERT INTO products (name, price_eur) VALUES ('Mechanical Keyboard', 80);
INSERT 0 1
testdb=# SELECT * FROM products;
        name         | price_eur | price_usd 
---------------------+-----------+-----------
 Mechanical Keyboard |        80 |     85.60
(1 row)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;example-3-returning-with-old-and-new-values&quot;&gt;Example 3: &lt;code&gt;RETURNING&lt;/code&gt; with &lt;code&gt;OLD&lt;/code&gt; and &lt;code&gt;NEW&lt;/code&gt; Values&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE price_audit (
    product_name TEXT,
    old_price NUMERIC,
    new_price NUMERIC,
    changed_at TIMESTAMPTZ DEFAULT now()
);

WITH updated AS (
    UPDATE products
    SET price_eur = 85
    WHERE name = 'Mechanical Keyboard'
    RETURNING OLD.name, OLD.price_eur AS old_price, NEW.price_eur AS new_price
)
INSERT INTO price_audit (product_name, old_price, new_price)
SELECT name, old_price, new_price FROM updated;

SELECT * FROM price_audit;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's like magic! No complex triggers or double queries needed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;testdb=# CREATE TABLE price_audit (
    product_name TEXT,
    old_price NUMERIC,
    new_price NUMERIC,
    changed_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE
testdb=# WITH updated AS (
    UPDATE products
    SET price_eur = 85
    WHERE name = 'Mechanical Keyboard'
    RETURNING OLD.name, OLD.price_eur AS old_price, NEW.price_eur AS new_price
)
INSERT INTO price_audit (product_name, old_price, new_price)
SELECT name, old_price, new_price FROM updated;
INSERT 0 1
testdb=# SELECT * FROM price_audit;
    product_name     | old_price | new_price |          changed_at           
---------------------+-----------+-----------+-------------------------------
 Mechanical Keyboard |        80 |        85 | 2025-09-30 09:16:50.260192+00
(1 row)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-experimenting-with-asynchronous-i-o-aio-&quot;&gt;Step 3: Experimenting with Asynchronous I/O (AIO)&lt;/h4&gt;
&lt;p&gt;Demonstrating a performance improvement like AIO in a simple example is tricky, as it heavily depends on your hardware (especially your disk speed). However, we can set up a small lab for you to experiment and measure the impact on your own system.&lt;/p&gt;
&lt;p&gt;With our Docker setup, we have the perfect lab: the &lt;code&gt;db_md5&lt;/code&gt; service starts with synchronous I/O (&lt;code&gt;io_method=sync&lt;/code&gt;), and the &lt;code&gt;db_scram&lt;/code&gt; service uses the PostgreSQL 18 default, which is with AIO enabled.&lt;/p&gt;
&lt;h5 id=&quot;1-prepare-a-large-dataset&quot;&gt;1. Prepare a Large Dataset&lt;/h5&gt;
&lt;p&gt;We'll use &lt;strong&gt;pgbench&lt;/strong&gt;, a tool included with PostgreSQL, to create test data in &lt;strong&gt;both&lt;/strong&gt; instances.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;From your terminal, run these two commands. Each will take a couple of minutes to populate its respective database with \~1.5 GB of data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Populate the database WITHOUT AIO (db_md5)
docker exec pg_client bash -c &quot;PGPASSWORD=password pgbench -i -s 100 -h db_md5 -U user testdb&quot;
  
# Populate the database WITH AIO (db_scram)
docker exec pg_client bash -c &quot;PGPASSWORD=password pgbench -i -s 100 -h db_scram -U user testdb&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;2-run-the-tests-and-compare&quot;&gt;2. Run the Tests and Compare&lt;/h5&gt;
&lt;p&gt;For a fair comparison, we'll first restart both containers to clear memory caches.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;From your terminal, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker restart postgres18_md5 postgres18_scram
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Wait a few seconds for the databases to start up.&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Now, enter the client container to run the tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it pg_client bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test on the server WITHOUT AIO:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Connect to db_md5 and measure the time
PGPASSWORD=password psql -h db_md5 -U user -d testdb -c &quot;\timing on&quot; -c &quot;SELECT count(*) FROM pgbench_accounts;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Take note of the time it returns.&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@5dfdfaef5e1a:/# PGPASSWORD=password psql -h db_md5 -U user -d testdb -c &quot;\timing on&quot; -c &quot;SELECT count(*) FROM pgbench_accounts;&quot;
Timing is on.
  count   
----------
 10000000
(1 row)
  
Time: 828.820 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test on the server WITH AIO:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Connect to db_scram and measure the time
PGPASSWORD=password psql -h db_scram -U user -d testdb -c &quot;\timing on&quot; -c &quot;SELECT count(*) FROM pgbench_accounts;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Take note of this second time.&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@5dfdfaef5e1a:/# PGPASSWORD=password psql -h db_scram -U user -d testdb -c &quot;\timing on&quot; -c &quot;SELECT count(*) FROM pgbench_accounts;&quot;
Timing is on.
  count   
----------
 10000000
(1 row)
  
Time: 539.800 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;3-analyze-the-results&quot;&gt;3. Analyze the Results&lt;/h5&gt;
&lt;p&gt;Compare the time from the second test (WITH AIO) with the first (WITHOUT AIO). On a system with fast disks (SSD/NVMe), you &lt;strong&gt;should see a noticeable reduction in execution time&lt;/strong&gt;. In my case, the time dropped from &lt;strong&gt;828.820 ms&lt;/strong&gt; to &lt;strong&gt;539.800 ms&lt;/strong&gt;, an improvement of &lt;strong&gt;34.87%&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The improvement you see will depend heavily on your hardware, operating system, and Docker configuration. The goal of this experiment is to give you the tools to measure it in your own environment.&lt;/p&gt;
&lt;h4 id=&quot;step-4-the-definitive-authentication-comparison&quot;&gt;Step 4: The Definitive Authentication Comparison&lt;/h4&gt;
&lt;p&gt;Now, let's create a user on each database to see the crucial difference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. On the MD5 (insecure) instance:&lt;/strong&gt;&lt;br&gt;
Connect to it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Terminal:&lt;/strong&gt; Inside the &lt;code&gt;pg_client&lt;/code&gt; container, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql -h db_md5 -U user -d testdb
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adminer:&lt;/strong&gt; Server: &lt;code&gt;db_md5&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And run the following SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE USER test_md5_user WITH PASSWORD 'a_simple_password';
SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'test_md5_user';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result will show the easily identifiable MD5 hash:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;testdb=# CREATE USER test_md5_user WITH PASSWORD 'a_simple_password';
WARNING:  setting an MD5-encrypted password
DETAIL:  MD5 password support is deprecated and will be removed in a future release of PostgreSQL.
HINT:  Refer to the PostgreSQL documentation for details about migrating to another password type.
CREATE ROLE
testdb=# SELECT rolname, rolpassword FROM pg


  [1]: https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=3116745045302959377
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/5803/postgresql-18-uncovered-asynchronous-i-o-uuidv7-and-pain-free-upgrades</guid>
<pubDate>Tue, 30 Sep 2025 13:04:23 +0000</pubDate>
</item>
<item>
<title>What actually happens when you run an SQL query?</title>
<link>https://coderlegion.com/5227/what-actually-happens-when-you-run-an-sql-query</link>
<description>&lt;p&gt;Do you know what actually happens when you run an SQL query?&lt;/p&gt;
&lt;p&gt;It’s not as simple as: “SQL just fetches rows from a table.”&lt;/p&gt;
&lt;p&gt;Behind the scenes, your query goes through a step-by-step execution order and knowing this is often an interview question for beginners.&lt;/p&gt;
&lt;p&gt;Execution Order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;FROM (pick the table)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WHERE (filter rows)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GROUP BY (make buckets)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAVING (filter buckets)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SELECT (choose what to show)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ORDER BY (sort the results)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LIMIT (control how much to show)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice how WHERE comes before grouping, while HAVING comes after grouping.&lt;/p&gt;
&lt;p&gt;Notice how ORDER BY is one of the last steps, even though we often write it after SELECT.&lt;/p&gt;
&lt;p&gt;Common Confusions:&lt;/p&gt;
&lt;p&gt;a) WHERE vs HAVING&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;WHERE filters rows before grouping.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAVING filters groups after aggregation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;WHERE city = 'Delhi' → filter students from Delhi first.&lt;/p&gt;
&lt;p&gt;HAVING COUNT(*) &amp;gt; 10 → only keep cities with more than 10 students.&lt;/p&gt;
&lt;p&gt;b) GROUP BY vs ORDER BY&lt;/p&gt;
&lt;p&gt;GROUP BY → bundles rows into categories (e.g., students per city).&lt;/p&gt;
&lt;p&gt;ORDER BY → decides the sequence in which results are displayed (e.g., sort cities by student count).&lt;/p&gt;
&lt;p&gt;c) LIMIT with ORDER BY&lt;/p&gt;
&lt;p&gt;Important tasks like “find top 3 highest salaries” or “top 5 cheapest products.”&lt;/p&gt;
&lt;p&gt;If you found this useful, drop a like or share it with someone preparing for SQL interviews.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@rahulphysicsmishra&quot;&gt;Follow&lt;/a&gt; for more such insightful posts , I’m breaking SQL down into small, clear lessons you’ll actually remember.&lt;/p&gt;
</description>
<category>Group By</category>
<guid isPermaLink="true">https://coderlegion.com/5227/what-actually-happens-when-you-run-an-sql-query</guid>
<pubDate>Sun, 07 Sep 2025 11:40:41 +0000</pubDate>
</item>
<item>
<title>Building an Order Processing Saga with MassTransit</title>
<link>https://coderlegion.com/4621/building-an-order-processing-saga-with-masstransit</link>
<description>&lt;p&gt;MassTransit has been around since 2007 and has matured into one of the most stable and feature-rich open-source .NET messaging frameworks.&lt;/p&gt;
&lt;p&gt;It acts like a factory for message brokers, meaning you can switch between providers such as RabbitMQ, Azure Service Bus, ActiveMQ, Amazon SQS, and others with minimal configuration changes.&lt;/p&gt;
&lt;p&gt;This makes it ideal for enterprise-grade distributed systems where you want flexibility without vendor lock-in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why MassTransit?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Stability: Over a decade of continuous development and production use.&lt;/p&gt;
&lt;p&gt;Multi-Broker Support: Easily swap between Azure Service Bus, RabbitMQ, Amazon SQS, etc., without rewriting business logic.&lt;/p&gt;
&lt;p&gt;Developer Friendly: Built for .NET developers with excellent documentation and integration with DI, EF Core, and ASP.NET Core.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Outbox pattern&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Request/Response messaging&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry and circuit-breaker policies&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sagas and State Machines&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Message scheduling&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;One of the Coolest Features: The State Machine&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A State Machine allows you to model complex workflows where messages arrive in any order, but your application processes them in a controlled, state-aware sequence.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8178334018060771430&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;For example, in this Order Processing Saga:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An order is submitted.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The saga waits for stock confirmation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If stock is reserved, the saga waits for payment authorization.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once payment is authorized, the order is marked as completed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If any step fails, the saga can transition to a failed state and trigger compensating actions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach eliminates spaghetti code for multi-step processes and makes workflows easy to visualize and maintain.&lt;/p&gt;
&lt;p&gt;Technology Stack Used in This Project&lt;br&gt;
.NET 9&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;MassTransit (with Saga State Machine)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RabbitMQ (message broker)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PostgreSQL (saga persistence)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Entity Framework Core (database access)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker (local RabbitMQ/PostgreSQL setup)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source Code&lt;br&gt;
Full working implementation available here:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/OutboxSagaDemo&quot;&gt; GitHub – OutboxSagaDemo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MassTransit Documentation – &lt;a rel=&quot;nofollow&quot; href=&quot;https://masstransit.io/&quot;&gt;https://masstransit.io/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MassTransit GitHub Repository – &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MassTransit/MassTransit&quot;&gt;https://github.com/MassTransit/MassTransit&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MassTransit Saga State Machine Guide – &lt;a rel=&quot;nofollow&quot; href=&quot;https://masstransit.io/documentation/patterns/saga&quot;&gt;https://masstransit.io/documentation/patterns/saga&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;RabbitMQ Documentation – &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rabbitmq.com/documentation.html&quot;&gt;https://www.rabbitmq.com/documentation.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;PostgreSQL Documentation – &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/&quot;&gt;https://www.postgresql.org/docs/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Entity Framework Core Docs – &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/4621/building-an-order-processing-saga-with-masstransit</guid>
<pubDate>Tue, 12 Aug 2025 09:22:53 +0000</pubDate>
</item>
<item>
<title>PostgreSQL Client Certificate Authentication: Complete Setup Guide</title>
<link>https://coderlegion.com/4373/postgresql-client-certificate-authentication-complete-setup-guide</link>
<description>&lt;p&gt;Client certificate authentication in PostgreSQL provides a secure, passwordless way to authenticate users. Instead of relying on passwords, clients present valid X.509 certificates to prove their identity.&lt;/p&gt;
&lt;h3 id=&quot;what-to-configure&quot;&gt;What to Configure&lt;/h3&gt;
&lt;h4 id=&quot;server-side-requirements&quot;&gt;Server-Side Requirements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSL certificates&lt;/strong&gt;: Server certificate + client certificates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL SSL settings&lt;/strong&gt;: Enable SSL and configure certificate paths&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication rules&lt;/strong&gt;: Configure &lt;code&gt;pg_hba.conf&lt;/code&gt; for certificate-based auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User mapping&lt;/strong&gt;: Link certificate Common Names to database users&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;client-side-requirements&quot;&gt;Client-Side Requirements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client certificate&lt;/strong&gt;: Valid X.509 certificate for the user&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private key&lt;/strong&gt;: Matching private key for the certificate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root certificate&lt;/strong&gt;: Server's certificate for verification&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection parameters&lt;/strong&gt;: Proper SSL mode and certificate paths&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-to-configure&quot;&gt;How to Configure&lt;/h3&gt;
&lt;h4 id=&quot;1-generate-ssl-certificates&quot;&gt;1. Generate SSL Certificates&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Create SSL directory
mkdir -p /var/lib/postgresql/17/main/ssl
cd /var/lib/postgresql/17/main/ssl

# Generate server certificate
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr \
  -subj &quot;/C=US/ST=State/L=City/O=Org/OU=OrgUnit/CN=localhost&quot;
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

# Generate client certificate for user 'appuser'
openssl genrsa -out appuser.key 4096
openssl req -new -key appuser.key -out appuser.csr \
  -subj &quot;/C=US/ST=State/L=City/O=Org/OU=OrgUnit/CN=appuser&quot;
openssl x509 -req -days 365 -in appuser.csr \
  -CA server.crt -CAkey server.key -CAcreateserial -out appuser.crt

# Set permissions
chown postgres:postgres *.crt *.key
chmod 600 *.key
chmod 644 *.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-configure-postgresql-ssl-settings&quot;&gt;2. Configure PostgreSQL SSL Settings&lt;/h4&gt;
&lt;p&gt;Add to &lt;code&gt;postgresql.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# SSL Configuration
ssl = on
ssl_cert_file = '/var/lib/postgresql/17/main/ssl/server.crt'
ssl_key_file = '/var/lib/postgresql/17/main/ssl/server.key'
ssl_ca_file = '/var/lib/postgresql/17/main/ssl/server.crt'
ssl_prefer_server_ciphers = on
ssl_min_protocol_version = 'TLSv1.2'
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-configure-authentication-rules&quot;&gt;3. Configure Authentication Rules&lt;/h4&gt;
&lt;p&gt;Update &lt;code&gt;pg_hba.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# SSL connections require client certificate authentication
hostssl all             all             0.0.0.0/0               cert clientcert=verify-full
hostssl all             all             ::/0                    cert clientcert=verify-full

# Non-SSL connections require password authentication
hostnossl all           all             0.0.0.0/0               scram-sha-256
hostnossl all           all             ::/0                    scram-sha-256
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-create-database-user&quot;&gt;4. Create Database User&lt;/h4&gt;
&lt;p&gt;The database user must match the certificate's Common Name(&lt;strong&gt;CN&lt;/strong&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE USER appuser WITH LOGIN;
GRANT CONNECT ON DATABASE mydb TO appuser;
-- Grant additional permissions as needed
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-to-connect-from-client&quot;&gt;How to Connect from Client&lt;/h3&gt;
&lt;h4 id=&quot;critical-connection-parameters&quot;&gt;Critical Connection Parameters&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Parameter     &lt;/th&gt;&lt;th&gt;Self-Managed Value            &lt;/th&gt;&lt;th&gt;RDS Value                           &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslmode&lt;/code&gt;     &lt;/td&gt;&lt;td&gt;&lt;code&gt;require&lt;/code&gt; or &lt;code&gt;verify-full&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;&lt;code&gt;require&lt;/code&gt; or &lt;code&gt;verify-full&lt;/code&gt;          &lt;/td&gt;&lt;td&gt;Force SSL connection&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslcert&lt;/code&gt;     &lt;/td&gt;&lt;td&gt;&lt;code&gt;/path/to/client.crt&lt;/code&gt;         &lt;/td&gt;&lt;td&gt;N/A (not supported)                 &lt;/td&gt;&lt;td&gt;Client certificate file&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslkey&lt;/code&gt;      &lt;/td&gt;&lt;td&gt;&lt;code&gt;/path/to/client.key&lt;/code&gt;         &lt;/td&gt;&lt;td&gt;N/A (not supported)                 &lt;/td&gt;&lt;td&gt;Client private key file&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslrootcert&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;/path/to/server.crt&lt;/code&gt;         &lt;/td&gt;&lt;td&gt;&lt;code&gt;./global-bundle.pem&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;Server certificate for verification&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;host&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;&lt;code&gt;localhost&lt;/code&gt; or custom         &lt;/td&gt;&lt;td&gt;&lt;code&gt;instance.region.rds.amazonaws.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Database server endpoint&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;password&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;Not required with client cert &lt;/td&gt;&lt;td&gt;Required (or IAM token)             &lt;/td&gt;&lt;td&gt;Authentication credential&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;connection-examples&quot;&gt;Connection Examples&lt;/h4&gt;
&lt;h5 id=&quot;self-managed-postgresql-with-client-certificates&quot;&gt;Self-Managed PostgreSQL with Client Certificates&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# psql - No password required
psql &quot;host=localhost port=5432 dbname=mydb user=appuser \
      sslmode=require \
      sslcert=./certs/appuser.crt \
      sslkey=./certs/appuser.key \
      sslrootcert=./certs/server.crt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// .NET - Client certificate authentication
var connectionString = &quot;Host=localhost;Port=5432;Database=mydb;Username=appuser;&quot; +
                      &quot;SSL Mode=Require;&quot; +
                      &quot;Client Certificate=./certs/appuser.crt;&quot; +
                      &quot;Client Key=./certs/appuser.key;&quot; +
                      &quot;Root Certificate=./certs/server.crt;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Python - Client certificate authentication
import psycopg2
conn = psycopg2.connect(
    host=&quot;localhost&quot;,
    port=5432,
    database=&quot;mydb&quot;,
    user=&quot;appuser&quot;,
    sslmode=&quot;require&quot;,
    sslcert=&quot;./certs/appuser.crt&quot;,
    sslkey=&quot;./certs/appuser.key&quot;,
    sslrootcert=&quot;./certs/server.crt&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java JDBC - Client certificate authentication
String url = &quot;jdbc:postgresql://localhost:5432/mydb&quot; +
            &quot;?ssl=true&quot; +
            &quot;&amp;amp;sslmode=require&quot; +
            &quot;&amp;amp;sslcert=./certs/appuser.crt&quot; +
            &quot;&amp;amp;sslkey=./certs/appuser.key&quot; +
            &quot;&amp;amp;sslrootcert=./certs/server.crt&quot; +
            &quot;&amp;amp;user=appuser&quot;;

Connection conn = DriverManager.getConnection(url);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;aws-rds-postgresql-with-ssl-password-required-&quot;&gt;AWS RDS PostgreSQL with SSL (Password Required)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# psql - Password required, verify server certificate
psql &quot;host=mydb.cluster-xyz.us-east-1.rds.amazonaws.com \
      port=5432 dbname=postgres user=myuser password=mypassword \
      sslmode=verify-full sslrootcert=./global-bundle.pem&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// .NET - RDS with SSL verification
var connectionString = &quot;Host=mydb.cluster-xyz.us-east-1.rds.amazonaws.com;&quot; +
                      &quot;Port=5432;Database=postgres;Username=myuser;Password=mypassword;&quot; +
                      &quot;SSL Mode=Require;Root Certificate=./global-bundle.pem;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Python - RDS with SSL verification  
import psycopg2
conn = psycopg2.connect(
    host=&quot;mydb.cluster-xyz.us-east-1.rds.amazonaws.com&quot;,
    port=5432,
    database=&quot;postgres&quot;,
    user=&quot;myuser&quot;,
    password=&quot;mypassword&quot;,
    sslmode=&quot;verify-full&quot;,
    sslrootcert=&quot;./global-bundle.pem&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java JDBC - RDS with SSL verification
String url = &quot;jdbc:postgresql://mydb.cluster-xyz.us-east-1.rds.amazonaws.com:5432/postgres&quot; +
            &quot;?ssl=true&quot; +
            &quot;&amp;amp;sslmode=verify-full&quot; +
            &quot;&amp;amp;sslrootcert=./global-bundle.pem&quot; +
            &quot;&amp;amp;user=myuser&quot; +
            &quot;&amp;amp;password=mypassword&quot;;

Connection conn = DriverManager.getConnection(url);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;server-configuration-vs-client-parameters&quot;&gt;Server Configuration vs Client Parameters&lt;/h3&gt;
&lt;h4 id=&quot;server-side-pg-hba-conf-&quot;&gt;Server Side (&lt;code&gt;pg_hba.conf&lt;/code&gt;)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Authentication method MUST be 'cert'
hostssl all all 0.0.0.0/0 cert clientcert=verify-full
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;client-side-connection-string-&quot;&gt;Client Side (Connection String)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# SSL mode MUST be 'require' or 'verify-full'
sslmode=require
# All three certificate files MUST be provided
sslcert=./client.crt
sslkey=./client.key  
sslrootcert=./server.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;common-pitfalls&quot;&gt;Common Pitfalls&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Certificate CN mismatch&lt;/strong&gt;: Client certificate CN must match database username&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing clientcert=verify-full&lt;/strong&gt;: Server won't require client certificates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrong SSL mode&lt;/strong&gt;: &lt;code&gt;sslmode=disable&lt;/code&gt; or &lt;code&gt;prefer&lt;/code&gt; may skip certificate auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File permissions&lt;/strong&gt;: Private keys must be readable only by the client process&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certificate expiration&lt;/strong&gt;: Monitor and rotate certificates before expiry&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;security-benefits&quot;&gt;Security Benefits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No password transmission&lt;/strong&gt;: Eliminates password-based attacks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strong authentication&lt;/strong&gt;: Based on cryptographic proof of identity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-repudiation&lt;/strong&gt;: Certificate-based audit trails&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mutual authentication&lt;/strong&gt;: Both client and server verify each other&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;aws-rds-postgresql-configuration&quot;&gt;AWS RDS PostgreSQL Configuration&lt;/h3&gt;
&lt;h4 id=&quot;rds-limitations-for-client-certificates&quot;&gt;RDS Limitations for Client Certificates&lt;/h4&gt;
&lt;p&gt;AWS RDS PostgreSQL has &lt;strong&gt;limited support&lt;/strong&gt; for client certificate authentication compared to self-managed PostgreSQL:&lt;/p&gt;
&lt;h5 id=&quot;what-s-not-available-in-rds&quot;&gt;What's NOT Available in RDS&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom client certificate generation&lt;/strong&gt;: You cannot upload custom client certificates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full &lt;code&gt;cert&lt;/code&gt; authentication method&lt;/strong&gt;: RDS doesn't support the &lt;code&gt;cert&lt;/code&gt; method in &lt;code&gt;pg_hba.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct certificate file access&lt;/strong&gt;: No access to &lt;code&gt;/var/lib/postgresql/&lt;/code&gt; directory&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;what-is-available-in-rds&quot;&gt;What IS Available in RDS&lt;/h5&gt;
&lt;h4 id=&quot;1-rds-ssl-server-certificates-amp-configuration&quot;&gt;1. RDS SSL Server Certificates &amp;amp; Configuration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Download RDS CA certificate bundle
wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem

# Enable SSL in RDS parameter group
aws rds modify-db-parameter-group \
    --db-parameter-group-name mygroup \
    --parameters &quot;ParameterName=ssl,ParameterValue=1,ApplyMethod=immediate&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-alternative-iam-database-authentication&quot;&gt;2. Alternative: IAM Database Authentication&lt;/h4&gt;
&lt;p&gt;For passwordless authentication in RDS, use IAM database authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Create IAM-enabled database user
CREATE USER iamuser;
GRANT rds_iam TO iamuser;
GRANT CONNECT ON DATABASE postgres TO iamuser;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Connect using IAM token (Python example)
import boto3
import psycopg2

rds_client = boto3.client('rds', region_name='us-east-1')
token = rds_client.generate_db_auth_token(
    DBHostname='mydb.xyz.us-east-1.rds.amazonaws.com',
    Port=5432,
    DBUsername='iamuser'
)

conn = psycopg2.connect(
    host=&quot;mydb.xyz.us-east-1.rds.amazonaws.com&quot;,
    port=5432,
    database=&quot;postgres&quot;,
    user=&quot;iamuser&quot;,
    password=token,  # IAM token as password
    sslmode=&quot;require&quot;,
    sslrootcert=&quot;./global-bundle.pem&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-hybrid-approach-alb-client-certificates-rds&quot;&gt;3. Hybrid Approach: ALB + Client Certificates + RDS&lt;/h4&gt;
&lt;p&gt;For applications requiring client certificate authentication with RDS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Client with Client Cert] → [ALB with SSL termination] → [App Server] → [RDS with IAM Auth]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ALB handles client certificate verification&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application validates certificates in code&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RDS connection uses IAM authentication&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;summary-amp-verification&quot;&gt;Summary &amp;amp; Verification&lt;/h3&gt;
&lt;h4 id=&quot;self-managed-vs-aws-rds-comparison&quot;&gt;Self-Managed vs AWS RDS Comparison&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature                     &lt;/th&gt;&lt;th&gt;Self-Managed          &lt;/th&gt;&lt;th&gt;AWS RDS&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Client Certificate Auth     &lt;/td&gt;&lt;td&gt;✅ Full support        &lt;/td&gt;&lt;td&gt;❌ Not supported&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Server SSL Certificates     &lt;/td&gt;&lt;td&gt;✅ Custom certificates &lt;/td&gt;&lt;td&gt;✅ AWS-managed certificates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Passwordless Authentication &lt;/td&gt;&lt;td&gt;✅ Client certificates &lt;/td&gt;&lt;td&gt;✅ IAM authentication&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Certificate Management      &lt;/td&gt;&lt;td&gt;❌ Manual rotation     &lt;/td&gt;&lt;td&gt;✅ Automatic rotation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SSL Configuration Control   &lt;/td&gt;&lt;td&gt;✅ Full control        &lt;/td&gt;&lt;td&gt;❌ Limited parameters&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;verification-commands&quot;&gt;Verification Commands&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Verify SSL is enabled
SHOW ssl;

-- Check active SSL connections with certificate details
SELECT datname, usename, client_addr, ssl, ssl_version, ssl_cipher,
       client_dn, issuer_dn
FROM pg_stat_ssl 
JOIN pg_stat_activity ON pg_stat_ssl.pid = pg_stat_activity.pid 
WHERE ssl = true;
-- Example output
pid|ssl |version|cipher                |bits|client_dn                                                      |client_serial                                   |issuer_dn                                                   |datid|datname       |pid|leader_pid|usesysid|usename       |application_name|client_addr|client_hostname|client_port|backend_start                |xact_start|query_start                  |state_change                 |wait_event_type|wait_event|state|backend_xid|backend_xmin|query_id           |query                                                                                                                        |backend_type  |
---+----+-------+----------------------+----+---------------------------------------------------------------+------------------------------------------------+------------------------------------------------------------+-----+--------------+---+----------+--------+--------------+----------------+-----------+---------------+-----------+-----------------------------+----------+-----------------------------+-----------------------------+---------------+----------+-----+-----------+------------+-------------------+-----------------------------------------------------------------------------------------------------------------------------+--------------+
754|true|TLSv1.3|TLS_AES_256_GCM_SHA384| 256|/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=redgatemonit|608692775915102986323080169170088721995325147053|/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=localhost|16384|redgatemonitor|754|          |   16385|redgatemonitor|psql            |10.88.0.1  |               |      59168|2025-07-30 09:43:20.240 +0100|          |2025-07-30 09:43:58.869 +0100|2025-07-30 09:43:58.870 +0100|Client         |ClientRead|idle |           |            |8885888923103386772|SELECT datname, usename, client_addr, ssl,* FROM pg_stat_ssl JOIN pg_stat_activity ON pg_stat_ssl.pid = pg_stat_activity.pid;|client backend|
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaways-1&quot;&gt;Key Takeaways&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Self-managed PostgreSQL&lt;/strong&gt;: Use client certificates for true passwordless authentication&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS RDS&lt;/strong&gt;: Combine IAM authentication with SSL encryption for secure connections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid approach&lt;/strong&gt;: Use ALB for client certificate verification with RDS backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Both approaches provide enterprise-grade security with different trade-offs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Client certificate authentication in PostgreSQL provides enterprise-grade security without the overhead of password management, making it ideal for automated systems and high-security environments.&lt;/p&gt;
&lt;h4 id=&quot;references&quot;&gt;References&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/ssl-tcp.html&quot;&gt;https://www.postgresql.org/docs/current/ssl-tcp.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4373/postgresql-client-certificate-authentication-complete-setup-guide</guid>
<pubDate>Fri, 01 Aug 2025 12:50:19 +0000</pubDate>
</item>
<item>
<title>What is SQL? A Beginner's Complete Guide</title>
<link>https://coderlegion.com/3880/what-is-sql-a-beginners-complete-guide</link>
<description>&lt;h3 id=&quot;follow-me-for-more-content-&quot;&gt;Follow Me for More Content!&lt;/h3&gt;
&lt;p&gt;Before we dive into SQL, let's connect! Follow me on these platforms for more programming tutorials and tech insights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Abdelhakim-Baalla&quot;&gt;Abdelhakim-Baalla&lt;/a&gt; - Check out my projects and code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/abdelhakimbaalla/&quot;&gt;abdelhakimbaalla&lt;/a&gt; - Professional updates and networking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Twitter (X):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/Abdelhakim99891&quot;&gt;@Abdelhakim99891&lt;/a&gt; - Quick tips and tech discussions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://abdelhakim-baalla.vercel.app/&quot;&gt;abdelhakim-baalla&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Have you ever wondered how websites store and retrieve information about users, products, or orders? Or how apps like Instagram know which photos to show you? The answer lies in databases, and the key to communicating with these databases is SQL.&lt;/p&gt;
&lt;p&gt;If you're completely new to programming or databases, don't worry! SQL is actually one of the most beginner-friendly programming languages you can learn. In this comprehensive guide, we'll explore what SQL is, why it's so important, and how you can start using it today.&lt;/p&gt;
&lt;p&gt;By the end of this post, you'll have a solid understanding of SQL fundamentals and be ready to start your database journey. Let's dive in!&lt;/p&gt;
&lt;h3 id=&quot;what-is-sql-&quot;&gt;What is SQL?&lt;/h3&gt;
&lt;p&gt;SQL stands for &lt;strong&gt;Structured Query Language&lt;/strong&gt; (pronounced &quot;S-Q-L&quot; or &quot;sequel&quot;). Think of it as a special language that allows you to communicate with databases. Just like you use English to talk to people, you use SQL to talk to databases.&lt;/p&gt;
&lt;h4 id=&quot;breaking-down-the-name&quot;&gt;Breaking Down the Name&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Structured:&lt;/strong&gt; SQL follows specific rules and formats&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query:&lt;/strong&gt; You ask questions (queries) to get information&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Language:&lt;/strong&gt; It's a programming language with its own syntax&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;simple-analogy&quot;&gt;Simple Analogy&lt;/h4&gt;
&lt;p&gt;Imagine a database as a massive digital filing cabinet with thousands of folders and documents. SQL is like having a super-efficient assistant who can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find any document you need in seconds&lt;/li&gt;
&lt;li&gt;Organize documents in any order you want&lt;/li&gt;
&lt;li&gt;Add new documents to the right folders&lt;/li&gt;
&lt;li&gt;Update information in existing documents&lt;/li&gt;
&lt;li&gt;Remove documents you no longer need&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-is-sql-important-&quot;&gt;Why is SQL Important?&lt;/h3&gt;
&lt;h4 id=&quot;1-universal-language-for-databases&quot;&gt;1. &lt;strong&gt;Universal Language for Databases&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;SQL is the standard language for working with databases. Whether you're using MySQL, PostgreSQL, SQL Server, or Oracle, the basic SQL concepts remain the same.&lt;/p&gt;
&lt;h4 id=&quot;2-high-demand-in-the-job-market&quot;&gt;2. &lt;strong&gt;High Demand in the Job Market&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;According to recent surveys, SQL consistently ranks among the top 5 most in-demand programming skills. Jobs requiring SQL skills often offer competitive salaries.&lt;/p&gt;
&lt;h4 id=&quot;3-foundation-for-data-analysis&quot;&gt;3. &lt;strong&gt;Foundation for Data Analysis&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;If you're interested in data science, business analytics, or any field involving data, SQL is your starting point.&lt;/p&gt;
&lt;h4 id=&quot;4-easy-to-learn&quot;&gt;4. &lt;strong&gt;Easy to Learn&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Unlike complex programming languages, SQL uses English-like words and phrases, making it incredibly beginner-friendly.&lt;/p&gt;
&lt;h3 id=&quot;how-databases-work-the-basics-&quot;&gt;How Databases Work (The Basics)&lt;/h3&gt;
&lt;p&gt;Before we dive deeper into SQL, let's understand what databases are and how they're organized.&lt;/p&gt;
&lt;h4 id=&quot;what-is-a-database-&quot;&gt;What is a Database?&lt;/h4&gt;
&lt;p&gt;A database is a structured collection of information stored electronically. Think of it like a digital spreadsheet, but much more powerful and organized.&lt;/p&gt;
&lt;h4 id=&quot;database-structure&quot;&gt;Database Structure&lt;/h4&gt;
&lt;p&gt;Databases are organized in a hierarchy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Database
├── Tables (like spreadsheet tabs)
│   ├── Rows (individual records)
│   └── Columns (categories of information)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;real-world-example&quot;&gt;Real-World Example&lt;/h4&gt;
&lt;p&gt;Let's say you're building an online bookstore. Your database might have these tables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Books Table:&lt;/strong&gt; Contains information about each book&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customers Table:&lt;/strong&gt; Stores customer information&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Orders Table:&lt;/strong&gt; Tracks customer purchases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authors Table:&lt;/strong&gt; Information about book authors&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;basic-sql-operations-crud-&quot;&gt;Basic SQL Operations (CRUD)&lt;/h3&gt;
&lt;p&gt;SQL operations are often referred to as CRUD operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;reate - Add new data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R&lt;/strong&gt;ead - Retrieve existing data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;U&lt;/strong&gt;pdate - Modify existing data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D&lt;/strong&gt;elete - Remove data&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;1-creating-data-insert-&quot;&gt;1. Creating Data (INSERT)&lt;/h4&gt;
&lt;p&gt;Adding new information to a database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Adding a new book to our bookstore
INSERT INTO books (title, author, price, genre)
VALUES ('The Great Gatsby', 'F. Scott Fitzgerald', 12.99, 'Fiction');
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-reading-data-select-&quot;&gt;2. Reading Data (SELECT)&lt;/h4&gt;
&lt;p&gt;Retrieving information from a database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Get all books
SELECT * FROM books;

-- Get only book titles and prices
SELECT title, price FROM books;

-- Get books under $15
SELECT * FROM books WHERE price &amp;lt; 15.00;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-updating-data-update-&quot;&gt;3. Updating Data (UPDATE)&lt;/h4&gt;
&lt;p&gt;Modifying existing information:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Update the price of a specific book
UPDATE books 
SET price = 14.99 
WHERE title = 'The Great Gatsby';
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-deleting-data-delete-&quot;&gt;4. Deleting Data (DELETE)&lt;/h4&gt;
&lt;p&gt;Removing information:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Remove a book from the database
DELETE FROM books 
WHERE title = 'The Great Gatsby';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-sql-concepts-every-beginner-should-know&quot;&gt;Key SQL Concepts Every Beginner Should Know&lt;/h3&gt;
&lt;h4 id=&quot;1-tables&quot;&gt;1. &lt;strong&gt;Tables&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Tables are the building blocks of databases. Each table represents a specific type of entity (like books, customers, or orders).&lt;/p&gt;
&lt;h4 id=&quot;2-primary-keys&quot;&gt;2. &lt;strong&gt;Primary Keys&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;A unique identifier for each row in a table. Like a social security number for database records.&lt;/p&gt;
&lt;h4 id=&quot;3-relationships&quot;&gt;3. &lt;strong&gt;Relationships&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Tables can be connected to each other. For example, an order table might reference a customer table.&lt;/p&gt;
&lt;h4 id=&quot;4-indexes&quot;&gt;4. &lt;strong&gt;Indexes&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Special structures that make searching faster. Like the index in a book that helps you find topics quickly.&lt;/p&gt;
&lt;h3 id=&quot;getting-started-with-sql&quot;&gt;Getting Started with SQL&lt;/h3&gt;
&lt;h4 id=&quot;step-1-choose-a-database-system&quot;&gt;Step 1: Choose a Database System&lt;/h4&gt;
&lt;p&gt;For beginners, I recommend starting with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQLite:&lt;/strong&gt; Simple, file-based database perfect for learning&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL:&lt;/strong&gt; Popular, free, and widely used&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL:&lt;/strong&gt; Powerful and feature-rich&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;step-2-install-and-set-up&quot;&gt;Step 2: Install and Set Up&lt;/h4&gt;
&lt;p&gt;Most database systems offer free community editions. You can also use online SQL playgrounds like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB Fiddle&lt;/li&gt;
&lt;li&gt;SQLiteOnline&lt;/li&gt;
&lt;li&gt;W3Schools SQL Tryit Editor&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;step-3-practice-with-sample-data&quot;&gt;Step 3: Practice with Sample Data&lt;/h4&gt;
&lt;p&gt;Start with simple examples:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Create a simple table
CREATE TABLE students (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    age INTEGER,
    grade TEXT
);

-- Add some data
INSERT INTO students (name, age, grade) VALUES
('Alice Johnson', 20, 'A'),
('Bob Smith', 19, 'B'),
('Carol Davis', 21, 'A');

-- Query the data
SELECT name, grade FROM students WHERE grade = 'A';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;common-sql-commands-reference&quot;&gt;Common SQL Commands Reference&lt;/h3&gt;
&lt;h4 id=&quot;data-retrieval&quot;&gt;Data Retrieval&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Basic selection
SELECT column1, column2 FROM table_name;

-- With conditions
SELECT * FROM table_name WHERE condition;

-- Sorting results
SELECT * FROM table_name ORDER BY column_name;

-- Limiting results
SELECT * FROM table_name LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;data-modification&quot;&gt;Data Modification&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Insert new record
INSERT INTO table_name (col1, col2) VALUES (value1, value2);

-- Update existing record
UPDATE table_name SET column1 = value1 WHERE condition;

-- Delete record
DELETE FROM table_name WHERE condition;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;real-world-applications-of-sql&quot;&gt;Real-World Applications of SQL&lt;/h3&gt;
&lt;h4 id=&quot;1-e-commerce-websites&quot;&gt;1. &lt;strong&gt;E-commerce Websites&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Track product inventory&lt;/li&gt;
&lt;li&gt;Process customer orders&lt;/li&gt;
&lt;li&gt;Manage user accounts&lt;/li&gt;
&lt;li&gt;Generate sales reports&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-social-media-platforms&quot;&gt;2. &lt;strong&gt;Social Media Platforms&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Store user profiles&lt;/li&gt;
&lt;li&gt;Manage posts and comments&lt;/li&gt;
&lt;li&gt;Track likes and shares&lt;/li&gt;
&lt;li&gt;Analyze user behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-banking-systems&quot;&gt;3. &lt;strong&gt;Banking Systems&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Process transactions&lt;/li&gt;
&lt;li&gt;Maintain account balances&lt;/li&gt;
&lt;li&gt;Generate financial reports&lt;/li&gt;
&lt;li&gt;Ensure data security&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-healthcare-systems&quot;&gt;4. &lt;strong&gt;Healthcare Systems&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Store patient records&lt;/li&gt;
&lt;li&gt;Track medical appointments&lt;/li&gt;
&lt;li&gt;Manage prescription data&lt;/li&gt;
&lt;li&gt;Generate health reports&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;best-practices-for-sql-beginners&quot;&gt;Best Practices for SQL Beginners&lt;/h3&gt;
&lt;h4 id=&quot;1-use-meaningful-names&quot;&gt;1. &lt;strong&gt;Use Meaningful Names&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Good
SELECT customer_name, order_date FROM orders;

-- Avoid
SELECT c, d FROM o;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-always-use-where-clauses-carefully&quot;&gt;2. &lt;strong&gt;Always Use WHERE Clauses Carefully&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Always test your WHERE conditions
SELECT * FROM customers WHERE customer_id = 123;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-start-simple&quot;&gt;3. &lt;strong&gt;Start Simple&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Begin with basic queries before moving to complex joins and subqueries.&lt;/p&gt;
&lt;h4 id=&quot;4-practice-regularly&quot;&gt;4. &lt;strong&gt;Practice Regularly&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Like any language, SQL improves with consistent practice.&lt;/p&gt;
&lt;h3 id=&quot;common-mistakes-to-avoid&quot;&gt;Common Mistakes to Avoid&lt;/h3&gt;
&lt;h4 id=&quot;1-forgetting-where-clauses&quot;&gt;1. &lt;strong&gt;Forgetting WHERE Clauses&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- This deletes ALL records!
DELETE FROM customers;

-- This deletes specific records
DELETE FROM customers WHERE customer_id = 123;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-not-backing-up-data&quot;&gt;2. &lt;strong&gt;Not Backing Up Data&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Always backup your database before making major changes.&lt;/p&gt;
&lt;h4 id=&quot;3-using-select-in-production-&quot;&gt;3. *&lt;em&gt;Using SELECT &lt;/em&gt; in Production**&lt;/h4&gt;
&lt;p&gt;Instead of selecting all columns, specify only the columns you need.&lt;/p&gt;
&lt;h4 id=&quot;4-ignoring-case-sensitivity&quot;&gt;4. &lt;strong&gt;Ignoring Case Sensitivity&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Some databases are case-sensitive. Be consistent with your naming.&lt;/p&gt;
&lt;h3 id=&quot;next-steps-in-your-sql-journey&quot;&gt;Next Steps in Your SQL Journey&lt;/h3&gt;
&lt;h4 id=&quot;1-advanced-topics-to-explore&quot;&gt;1. &lt;strong&gt;Advanced Topics to Explore&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Joins (connecting multiple tables)&lt;/li&gt;
&lt;li&gt;Indexes (improving query performance)&lt;/li&gt;
&lt;li&gt;Stored procedures (reusable code blocks)&lt;/li&gt;
&lt;li&gt;Triggers (automatic actions)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-practice-resources&quot;&gt;2. &lt;strong&gt;Practice Resources&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;HackerRank SQL challenges&lt;/li&gt;
&lt;li&gt;LeetCode database problems&lt;/li&gt;
&lt;li&gt;SQLBolt interactive tutorial&lt;/li&gt;
&lt;li&gt;Codecademy SQL course&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-build-projects&quot;&gt;3. &lt;strong&gt;Build Projects&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Create a personal expense tracker&lt;/li&gt;
&lt;li&gt;Build a simple library management system&lt;/li&gt;
&lt;li&gt;Design a blog database structure&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-learn-related-technologies&quot;&gt;4. &lt;strong&gt;Learn Related Technologies&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Database design principles&lt;/li&gt;
&lt;li&gt;Data modeling&lt;/li&gt;
&lt;li&gt;Business intelligence tools&lt;/li&gt;
&lt;li&gt;Cloud database services (AWS RDS, Google Cloud SQL)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;tools-and-resources&quot;&gt;Tools and Resources&lt;/h3&gt;
&lt;h4 id=&quot;free-learning-resources&quot;&gt;Free Learning Resources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;W3Schools SQL Tutorial:&lt;/strong&gt; Comprehensive and beginner-friendly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLBolt:&lt;/strong&gt; Interactive SQL lessons&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;freeCodeCamp:&lt;/strong&gt; Free SQL course on YouTube&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Khan Academy:&lt;/strong&gt; Database basics&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;practice-platforms&quot;&gt;Practice Platforms&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HackerRank:&lt;/strong&gt; SQL challenges and competitions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LeetCode:&lt;/strong&gt; Database problems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLZoo:&lt;/strong&gt; Interactive SQL tutorials&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DB Fiddle:&lt;/strong&gt; Online SQL playground&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;books-for-deeper-learning&quot;&gt;Books for Deeper Learning&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Learning SQL&quot; by Alan Beaulieu&lt;/li&gt;
&lt;li&gt;&quot;SQL in 10 Minutes, Sams Teach Yourself&quot; by Ben Forta&lt;/li&gt;
&lt;li&gt;&quot;Head First SQL&quot; by Lynn Beighley&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;SQL is your gateway to the world of databases and data management. What makes SQL special is its simplicity and power – you can start writing useful queries within hours of learning the basics.&lt;/p&gt;
&lt;p&gt;Remember these key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL is the universal language for databases&lt;/li&gt;
&lt;li&gt;It uses English-like syntax that's easy to learn&lt;/li&gt;
&lt;li&gt;The four basic operations (CRUD) form the foundation&lt;/li&gt;
&lt;li&gt;Practice with real data to build confidence&lt;/li&gt;
&lt;li&gt;Start simple and gradually tackle more complex concepts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The journey of learning SQL is incredibly rewarding. Whether you're interested in web development, data analysis, or just want to add a valuable skill to your toolkit, SQL is an excellent investment in your future.&lt;/p&gt;
&lt;p&gt;Don't let the technical aspects intimidate you – every SQL expert started exactly where you are now. The key is to start practicing with simple examples and gradually build your skills.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ready to start your SQL journey?&lt;/strong&gt; Pick one of the online SQL playgrounds mentioned above and try creating your first table today. You'll be amazed at how quickly you progress!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;stay-connected-for-more-learning-content-&quot;&gt;Stay Connected for More Learning Content!&lt;/h3&gt;
&lt;p&gt;If you found this guide helpful, I'd love to have you join my community of learners! Follow me on these platforms for more programming tutorials, career advice, and tech insights:&lt;/p&gt;
&lt;h4 id=&quot;don-t-miss-these-&quot;&gt;Don't Miss These!&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Abdelhakim-Baalla&quot;&gt;Abdelhakim-Baalla&lt;/a&gt; - Browse my code and projects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/abdelhakimbaalla/&quot;&gt;abdelhakimbaalla&lt;/a&gt; - Professional insights and networking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Twitter (X):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/Abdelhakim99891&quot;&gt;@Abdelhakim99891&lt;/a&gt; - Daily tech tips and discussions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instagram:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.instagram.com/abdelhakim.baalla/&quot;&gt;@abdelhakim.baalla&lt;/a&gt; - Behind-the-scenes content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dev.to:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/abdelhakim_baalla&quot;&gt;abdelhakim_baalla&lt;/a&gt; - In-depth tutorials&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Medium:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@abdelhakimbaalla50&quot;&gt;@abdelhakimbaalla50&lt;/a&gt; - Detailed articles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reddit:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.reddit.com/user/Abdelhakim-Baalla430/&quot;&gt;Abdelhakim-Baalla430&lt;/a&gt; - Community discussions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://abdelhakim-baalla.vercel.app/&quot;&gt;abdelhakim-baalla.vercel.app&lt;/a&gt; - My work showcase&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Substack:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://substack.com/@abdelhakimbaalla&quot;&gt;@abdelhakimbaalla&lt;/a&gt; - Newsletter content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hashnode:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://abdelhakimbaalla.hashnode.dev/&quot;&gt;abdelhakimbaalla.hashnode.dev&lt;/a&gt; - Technical blog&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;YouTube:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/@Dinocrafting&quot;&gt;@Dinocrafting&lt;/a&gt; - Video tutorials&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stack Overflow:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://stackoverflow.com/users/28245534/abdelhakim-baalla?tab=profile&quot;&gt;Profile&lt;/a&gt; - Q&amp;amp;A and help&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Your support means the world to me!&lt;/strong&gt;  Follow for more beginner-friendly programming content, project tutorials, and career guidance.&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/3880/what-is-sql-a-beginners-complete-guide</guid>
<pubDate>Thu, 10 Jul 2025 10:56:55 +0000</pubDate>
</item>
<item>
<title>PostgreSQL 18 Beta 1 Summary</title>
<link>https://coderlegion.com/3636/postgresql-18-beta-1-summary</link>
<description>&lt;h2 id=&quot;postgresql-18-beta-1---new-features-and-breaking-changes&quot;&gt;PostgreSQL 18 Beta 1 - New Features and Breaking Changes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nori-shinoda/documents/blob/main/postgresql_18_beta1_new_features_en_20250527-1.pdf&quot;&gt;GitHub - PostgreSQL 18 Beta 1 New Features&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;PostgreSQL 18 introduces over 200 new features and changes. This document summarizes all significant new features, improvements, and breaking changes introduced in the Beta 1 release. Meaning these features can disappear too, this is just a beta.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-highlights-of-major-new-features&quot;&gt;1. Highlights of Major New Features&lt;/h3&gt;
&lt;h4 id=&quot;1-1-adaptation-to-large-scale-environments&quot;&gt;1.1 Adaptation to Large Scale Environments&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Virtual Columns&lt;/strong&gt;: Support for virtual (non-stored) columns in tables.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ONLY Clause for VACUUM/ANALYZE&lt;/strong&gt;: Now supports partitioned tables to avoid recursive partition scans.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;1-2-reliability-enhancements&quot;&gt;1.2 Reliability Enhancements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pg_dumpall&lt;/code&gt; Output Format&lt;/strong&gt;: Supports formats other than plain text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pg_verifybackup&lt;/code&gt; Tar Format&lt;/strong&gt;: Adds support for tar format verification.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;1-3-maintainability-improvements&quot;&gt;1.3 Maintainability Improvements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Statistics Migration&lt;/strong&gt;: &lt;code&gt;pg_dump&lt;/code&gt; and &lt;code&gt;pg_dumpall&lt;/code&gt; can now include optimizer statistics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functions for Stats Management&lt;/strong&gt;: Added functions to set or clear optimizer stats per table/column.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_restore_relation_stats&lt;/code&gt;, &lt;code&gt;pg_clear_relation_stats&lt;/code&gt;, &lt;code&gt;pg_restore_attribute_stats&lt;/code&gt;, &lt;code&gt;pg_clear_attribute_stats&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Commits: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=d32d1463995c036853eeb9ec99cc367ffc7794ae&quot;&gt;d32d146&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;1-4-programming-improvements&quot;&gt;1.4 Programming Improvements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Access to &lt;code&gt;OLD&lt;/code&gt; and &lt;code&gt;NEW&lt;/code&gt; in &lt;code&gt;RETURNING&lt;/code&gt; clause&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New Function&lt;/strong&gt;: UUID v7 support.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;1-5-preparations-for-future-features&quot;&gt;1.5 Preparations for Future Features&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Asynchronous I/O Infrastructure&lt;/strong&gt;: Introduced with &lt;code&gt;io_method&lt;/code&gt; and &lt;code&gt;io_workers&lt;/code&gt;.&lt;ul&gt;
&lt;li&gt;Commits: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=da7226993fd4b73d8b40abb7167d124eada97f2e&quot;&gt;da72269&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NUMA Support&lt;/strong&gt;: Basic infrastructure and views (&lt;code&gt;pg_shmem_allocations_numa&lt;/code&gt;).&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=8cc139bec34a2971b0682a04eb52ce7b3f5bb425&quot;&gt;8cc139b&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-breaking-changes-amp-incompatibilities&quot;&gt;2. Breaking Changes &amp;amp; Incompatibilities&lt;/h3&gt;
&lt;h4 id=&quot;2-1-platform-amp-tool-support&quot;&gt;2.1 Platform &amp;amp; Tool Support&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Dropped support for: PA-RISC, LLVM &amp;lt; 14, OpenSSL &amp;lt; 1.1.1, Python &amp;lt; 3.6.8.&lt;/li&gt;
&lt;li&gt;Added support: Tcl 9.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-2-passwords&quot;&gt;2.2 Passwords&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MD5 passwords&lt;/strong&gt;: Now deprecated; warning shown by default.&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=db6a4a985bc09d260d5c29848e3c97f080646a53&quot;&gt;db6a4a9&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-3-configuration&quot;&gt;2.3 Configuration&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Removed &lt;code&gt;--disable-spin-locks&lt;/code&gt; and &lt;code&gt;--disable-atomics&lt;/code&gt;.&lt;ul&gt;
&lt;li&gt;Commits: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=e25626677f8076eb3ce94586136c5464ee154381&quot;&gt;e256266&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initdb&lt;/code&gt; now enables data checksums by default.&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=04bec894a04cb0d32533f1522ab81b7016141ff1&quot;&gt;04bec89&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-4-sql-amp-ddl-statements&quot;&gt;2.4 SQL &amp;amp; DDL Statements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;COPY FREEZE&lt;/strong&gt;: Disallowed on foreign tables.&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=401a6956fa69c9202cbc14c09ba8a9c430b90cac&quot;&gt;401a695&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EXPLAIN&lt;/strong&gt;: &lt;code&gt;BUFFERS&lt;/code&gt; enabled by default.&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=c2a4078ebad71999dd451ae7d4358be3c9290b07&quot;&gt;c2a4078&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CREATE SUBSCRIPTION&lt;/strong&gt;: Default &lt;code&gt;streaming&lt;/code&gt; = &lt;code&gt;parallel&lt;/code&gt;.&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1bf1140be87230c71d0e7b29939f7e2b3d073aa1&quot;&gt;1bf1140&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-architecture-changes&quot;&gt;3. Architecture Changes&lt;/h3&gt;
&lt;h4 id=&quot;3-1-catalog-changes&quot;&gt;3.1 Catalog Changes&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New Views&lt;/strong&gt;: &lt;code&gt;pg_aios&lt;/code&gt;, &lt;code&gt;pg_shmem_allocations_numa&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Added Columns&lt;/strong&gt;: Many views now include additional metadata, such as constraint enforcement flags, VACUUM/ANALYZE timing, and I/O stats.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-2-logical-replication&quot;&gt;3.2 Logical Replication&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Logs detailed conflict types like &lt;code&gt;insert_exists&lt;/code&gt;, &lt;code&gt;update_differ&lt;/code&gt;, &lt;code&gt;delete_missing&lt;/code&gt;.&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=9758174e2e5cd278cf37e0980da76b51890e0011&quot;&gt;9758174&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Propagation of generated columns via &lt;code&gt;publish_generated_columns&lt;/code&gt;.&lt;ul&gt;
&lt;li&gt;Commit: &lt;a rel=&quot;nofollow&quot; href=&quot;https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=7054186c4ebe24e63254651e2ae9b36efae90d4e&quot;&gt;7054186&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-3-storage-i-o-enhancements&quot;&gt;3.3 Storage I/O Enhancements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Streaming I/O&lt;/strong&gt;: Broadened scope to VACUUM, CREATE DATABASE, GiST/SP-GiST, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async I/O&lt;/strong&gt;: Controlled with &lt;code&gt;io_method&lt;/code&gt;, can use &lt;code&gt;io_uring&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-sql-enhancements&quot;&gt;4. SQL Enhancements&lt;/h3&gt;
&lt;h4 id=&quot;4-1-alter-enhancements&quot;&gt;4.1 ALTER Enhancements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ALTER CONSTRAINT SET INHERIT/NO INHERIT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ADD CONSTRAINT ... NOT VALID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ALTER TABLE ONLY DROP CONSTRAINT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-2-analyze-improvements&quot;&gt;4.2 ANALYZE Improvements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ONLY Clause&lt;/strong&gt;: Skip partitions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verbose Stats&lt;/strong&gt;: Includes I/O, WAL, and delay stats.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-utilities-amp-tooling&quot;&gt;5. Utilities &amp;amp; Tooling&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_combinebackup&lt;/code&gt;, &lt;code&gt;pg_createsubscriber&lt;/code&gt;: New utilities.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pg_stat_io&lt;/code&gt;: Combines WAL and I/O metrics.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;psql \conninfo&lt;/code&gt;: More detailed output.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-contrib-modules&quot;&gt;6. Contrib Modules&lt;/h3&gt;
&lt;p&gt;Highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;amcheck&lt;/code&gt;: Enhanced with streaming I/O.&lt;/li&gt;
&lt;li&gt;Enhancements to: &lt;code&gt;pgcrypto&lt;/code&gt;, &lt;code&gt;pg_stat_statements&lt;/code&gt;, &lt;code&gt;postgres_fdw&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;PostgreSQL 18 Beta 1 introduces significant architectural upgrades and functional improvements while laying the groundwork for upcoming innovations like async I/O and NUMA-aware processing. Compatibility testing is essential for a smooth transition.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/3636/postgresql-18-beta-1-summary</guid>
<pubDate>Sun, 29 Jun 2025 18:56:59 +0000</pubDate>
</item>
<item>
<title>Crystal DBA brings AI-powered PostgreSQL expertise to dev teams, bridging the database optimization skills gap.</title>
<link>https://coderlegion.com/2954/crystal-brings-powered-postgresql-expertise-teams-bridging-database-optimization-skills</link>
<description>&lt;h2 id=&quot;crystal-dba-ai-powered-postgresql-optimization-for-development-teams&quot;&gt;Crystal DBA: AI-Powered PostgreSQL Optimization for Development Teams&lt;/h2&gt;
&lt;p&gt;In the ever-evolving database landscape, PostgreSQL has become a powerhouse for application development, but mastering its nuances presents significant challenges. During the 60th IT Press Tour, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.crystaldba.ai/&quot;&gt;Crystal DBA&lt;/a&gt; unveiled their innovative approach to democratizing PostgreSQL expertise through AI, offering developers and engineers a way to optimize database performance without specialized knowledge.&lt;/p&gt;
&lt;h3 id=&quot;the-postgresql-expertise-gap&quot;&gt;The PostgreSQL Expertise Gap&lt;/h3&gt;
&lt;p&gt;PostgreSQL's popularity continues to surge across development teams, but running it efficiently requires specialized skills that are increasingly hard to find. This expertise gap creates a critical bottleneck for engineering teams.&lt;/p&gt;
&lt;p&gt;As &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/jssmith/&quot;&gt;Johann Schleier-Smith&lt;/a&gt;, CEO and founder of Crystal DBA explained, &quot;PostgreSQL has exploded in popularity, but it takes a fairly specialized skill set to run it really well. Most companies don't have access to that level of expertise.&quot;&lt;/p&gt;
&lt;p&gt;Schleier-Smith brings credible experience to this problem space. As former CTO of if(we), he scaled social platforms to 300 million members while navigating the complexities of various database technologies, including PostgreSQL.&lt;/p&gt;
&lt;h3 id=&quot;crystal-dba-your-ai-database-teammate&quot;&gt;Crystal DBA: Your AI Database Teammate&lt;/h3&gt;
&lt;p&gt;Crystal DBA positions itself as neither just a chatbot nor a fully autonomous agent, but rather an &quot;AI teammate&quot; that works alongside engineers and developers. This teammate approach provides specialized PostgreSQL knowledge on demand while integrating with existing workflows through Slack, Teams, web interfaces, or terminal access.&lt;/p&gt;
&lt;p&gt;&quot;The teammate metaphor works best for us,&quot; explained Schleier-Smith. &quot;It sits in your Slack or chat, understands everything that's going on, and helps you get the job done. But it's not here to take your job—it's here to help you do your job better.&quot;&lt;/p&gt;
&lt;h3 id=&quot;technical-implementation&quot;&gt;Technical Implementation&lt;/h3&gt;
&lt;p&gt;The platform combines three technological approaches:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Generative AI&lt;/strong&gt;: Powers the conversational interface and reasoning capabilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictive AI&lt;/strong&gt;: Analyzes workload patterns and predicts optimization opportunities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Classical Optimization&lt;/strong&gt;: Implements proven database tuning algorithms&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This tech stack enables several key capabilities that directly benefit development teams:&lt;/p&gt;
&lt;h4 id=&quot;query-analysis-and-optimization&quot;&gt;Query Analysis and Optimization&lt;/h4&gt;
&lt;p&gt;During the demo, Crystal DBA demonstrated how it can identify performance bottlenecks in SQL queries by analyzing execution plans and suggesting improvements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- From a simple question like:
-- &quot;What are the three most time-consuming queries running on this system?&quot;

-- Crystal DBA can run diagnostic commands like:
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 3;

-- Then provide optimizations including:
-- - Index recommendations
-- - Query structure improvements
-- - Configuration parameter adjustments
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;configuration-management&quot;&gt;Configuration Management&lt;/h4&gt;
&lt;p&gt;PostgreSQL has over 300 configuration parameters across categories like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache and Buffer Management (37 parameters)&lt;/li&gt;
&lt;li&gt;Write-Ahead Log (38 parameters)&lt;/li&gt;
&lt;li&gt;Query Tuning (48 parameters)&lt;/li&gt;
&lt;li&gt;Vacuum (13 parameters)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Crystal DBA can recommend optimal settings based on workload characteristics, helping engineers avoid the complexity of manual tuning while aligning configuration with application requirements.&lt;/p&gt;
&lt;h4 id=&quot;automated-maintenance-recommendations&quot;&gt;Automated Maintenance Recommendations&lt;/h4&gt;
&lt;p&gt;The platform can analyze database behavior to suggest maintenance routines, particularly around vacuum operations—PostgreSQL's method for reclaiming storage and maintaining performance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;The autovacuum settings appear to be causing inefficient cleanup cycles. Consider adjusting autovacuum_vacuum_threshold from 50 to 200 and autovacuum_vacuum_scale_factor from 0.2 to 0.1 to better match your workload patterns.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;security-and-enterprise-readiness&quot;&gt;Security and Enterprise Readiness&lt;/h3&gt;
&lt;p&gt;Crystal DBA addresses security concerns through several approaches:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read-only operation&lt;/strong&gt;: The system only executes read-only diagnostic commands by default&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment flexibility&lt;/strong&gt;: Options include cloud-based, on-premises with LLaMa models, or hybrid approaches&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PII protection&lt;/strong&gt;: Filters prevent sensitive data from being processed by the AI system&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/neilthombre/&quot;&gt;Neil Thombre&lt;/a&gt;, VP of Engineering at Crystal DBA and former database development lead at Amazon, emphasized: &quot;All the predicates are obfuscated as $1, $2—just column names. One could argue column names are also PII but that's what you're asking LLM to tune. You're never going to show values.&quot;&lt;/p&gt;
&lt;h3 id=&quot;pricing-and-availability&quot;&gt;Pricing and Availability&lt;/h3&gt;
&lt;p&gt;Crystal DBA offers a free tier for individuals that provides real-time analysis without historical data collection. A team tier at $200/month per production server includes continuous monitoring, historical analysis, and Slack integration. Enterprise pricing is customized based on deployment requirements.&lt;/p&gt;
&lt;h3 id=&quot;use-cases-for-development-teams&quot;&gt;Use Cases for Development Teams&lt;/h3&gt;
&lt;p&gt;The solution addresses several pain points for developers and engineering teams:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scaling challenges&lt;/strong&gt;: Ensuring database performance as application demand grows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost optimization&lt;/strong&gt;: Identifying opportunities to reduce infrastructure expenses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time efficiency&lt;/strong&gt;: Reducing time spent on database troubleshooting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version upgrades&lt;/strong&gt;: Simplifying the complex process of PostgreSQL version migration&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As one attendee commented, &quot;It's like no-code for databases. I can tell your agent to set up a database for an ERP system.&quot;&lt;/p&gt;
&lt;h3 id=&quot;market-position-and-differentiation&quot;&gt;Market Position and Differentiation&lt;/h3&gt;
&lt;p&gt;While solutions like Datadog, SolarWinds, and pganalyze offer PostgreSQL monitoring, Crystal DBA differentiates itself through its AI-first approach focused on making database expertise accessible to generalist engineers rather than database specialists.&lt;/p&gt;
&lt;p&gt;The seed-funded startup (reportedly $7 million from Amplify Partners) currently focuses exclusively on PostgreSQL but acknowledges potential expansion to MySQL, Oracle, and SQL Server in the future.&lt;/p&gt;
&lt;h3 id=&quot;the-future-of-database-management&quot;&gt;The Future of Database Management&lt;/h3&gt;
&lt;p&gt;Crystal DBA represents an emerging trend in database management—using AI to augment human expertise rather than replace it. By embedding specialized knowledge directly into engineering workflows, it addresses a critical skills gap while allowing development teams to focus on building applications rather than tuning databases.&lt;/p&gt;
&lt;p&gt;As PostgreSQL continues its rise as a leading database technology, tools like Crystal DBA may become essential components in the modern developer's toolkit.&lt;/p&gt;
</description>
<category>Data Science</category>
<guid isPermaLink="true">https://coderlegion.com/2954/crystal-brings-powered-postgresql-expertise-teams-bridging-database-optimization-skills</guid>
<pubDate>Sat, 10 May 2025 23:55:20 +0000</pubDate>
</item>
<item>
<title>PostgreSQL Change data capture (CDC) + golang sample code.</title>
<link>https://coderlegion.com/1788/postgresql-change-data-capture-cdc-golang-sample-code</link>
<description>&lt;h2 id=&quot;postgresql-change-data-capture-cdc-golang-sample-code-&quot;&gt;PostgreSQL Change data capture (CDC) + golang sample code.&lt;/h2&gt;
&lt;p&gt; &lt;img src=&quot;https://dbconvert.com/blog/content/images/2022/03/PostgresWAL.drawio-1.png&quot; alt=&quot;PostgreSQL Change data capture (CDC)&quot;&gt;&lt;/p&gt;
&lt;p&gt;The architecture of modern web applications consists of several software components such as dashboards, analytics, databases, data lakes, caches, search, etc.&lt;/p&gt;
&lt;p&gt;The database is usually the core part of any application. Real-time data updates keep disparate data systems in continuous sync and respond quickly to new information. So how to keep your application ecosystem in sync? How do these other components get information about changes in the database? &lt;strong&gt;Change Data Capture&lt;/strong&gt; or &lt;strong&gt;CDC&lt;/strong&gt; refers to any solution that identifies new or changed data.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;This post is about PostgreSQL CDC&lt;/strong&gt;  &lt;strong&gt;and the ways to achieve this.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Change data capture (CDC)&lt;/strong&gt; is an approach to data integration to detect, capture, and deliver the changes made to database data sources.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In General, CDC-based data integration consists of the following steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Capture change data in a source database.&lt;/li&gt;
&lt;li&gt;Convert the changed data to a format that your consumers can accept.&lt;/li&gt;
&lt;li&gt;Publish the data to consumers or target database.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;PostgreSQL offers two built-in ways to make CDC possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;From &lt;em&gt;transaction logs,&lt;/em&gt; PostgreSQL WALs, aka Write Ahead Logs.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Database Triggers.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's briefly discuss the pros and cons of using Transaction logs (WALs) and Triggers to capture data changes.&lt;/p&gt;
&lt;h4 id=&quot;triggers-&quot;&gt;Triggers.&lt;/h4&gt;
&lt;p&gt;Trigger-based methods involve creating audit triggers on the database to capture all the events related to INSERT, UPDATE and DELETE methods.&lt;/p&gt;
&lt;p&gt;Triggers can be attached to tables (partitioned or not) or views.&lt;/p&gt;
&lt;p&gt;Triggers can also fire for TRUNCATE statements. If a trigger event occurs, the trigger's function is called at the appropriate time to handle the event.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The most important advantage of this method is that all of this can be done at the SQL level, unlike transaction logs.&lt;/li&gt;
&lt;li&gt;However, the use of triggers has a significant impact on the performance of the source database, because these triggers need to be run on the application database when changes are made to the data.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;transaction-logs&quot;&gt;Transaction Logs&lt;/h4&gt;
&lt;p&gt;On the other hand, for modern DBMS, &lt;em&gt;transaction logs (WAL for PostgreSQL)&lt;/em&gt; are commonly used for &lt;strong&gt;transaction logging&lt;/strong&gt; , replication, and REDO.&lt;/p&gt;
&lt;p&gt;In PostgreSQL, all transactions like INSERT, UPDATE, DELETE, and DDLs are written to the WAL before the client sends the transaction result.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The advantage of this approach is that &lt;strong&gt;it does not affect the performance of the database in any way.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;It also requires no modification to DB tables or the application. There is no need to create additional tables in the source database.&lt;/li&gt;
&lt;li&gt;Log-based CDC is generally considered the superior approach to change data capture applicable to all possible scenarios, including systems with extremely high transaction volumes.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;If you want row-by-row streaming of Postgres data changes as they happen, you'll need &lt;em&gt;Logical Decoding&lt;/em&gt; or &lt;em&gt;Postgres logical replication feature&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ay6clxrnsg4ekkxs8593.gif&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;using-postgres-logical-decoding-&quot;&gt;Using Postgres Logical Decoding.&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/logicaldecoding-example.html&quot;&gt;Logical decoding&lt;/a&gt; is the official name of PostgreSQL's log-based CDC (Logical Replication).&lt;/p&gt;
&lt;p&gt;Logical decoding uses the content of the PostgreSQL Write-Ahead Log to store all of the activities that occur in the database. Write Ahead Log is an internal log that describes database changes on a storage level.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The first step in using logical decoding is to set the following parameters in the Postgres configuration &lt;code&gt;postgresql.conf&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;wal_level = logical
max_replication_slots = 5
max_wal_senders = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Setting &lt;code&gt;wal_level&lt;/code&gt; to &lt;code&gt;logical&lt;/code&gt; allows the WAL to record information needed for logical decoding.&lt;/li&gt;
&lt;li&gt;Ensure that your &lt;code&gt;max_replication_slots&lt;/code&gt; value is equal to or higher than the number of PostgreSQL connectors that use WAL &lt;em&gt;plus&lt;/em&gt; the number of other replication slots your database uses.&lt;/li&gt;
&lt;li&gt;Ensure that the &lt;code&gt;max_wal_senders&lt;/code&gt; parameter, which specifies the maximum number of concurrent connections to the WAL, is at least twice the number of logical replication slots. For example, if your database uses 5 replication slots in total, the &lt;code&gt;max_wal_senders&lt;/code&gt; value must be 10 or greater.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Restart your Postgres server to apply the changes.&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The second step is to set up logical replication using the output plugin &lt;code&gt;test_decoding&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Create a logical replication slot for the database you want to sync by running the following command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT pg_create_logical_replication_slot('replication_slot', 'test_decoding');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;Each replication &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/10/warm-standby.html#STREAMING-REPLICATION-SLOTS-MANIPULATION&quot;&gt;slot has a name, which can contain lower-case letters, numbers, and the underscore character.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To verify the slot is created successfully run the following command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT slot_name, plugin, slot_type, database, active, restart_lsn, confirmed_flush_lsn FROM pg_replication_slots;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;In the next step create a publication for all your tables or only specific ones. If you specify tables, you add or remove tables from the publication later.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE PUBLICATION pub FOR ALL TABLES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE PUBLICATION pub FOR TABLE table1, table2, table3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optionally you can choose which operations to include in the publication. For example, the following publication includes only INSERT and UPDATE operations for &lt;code&gt;table1&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE PUBLICATION insert_update_only_pub FOR TABLE table1 WITH (publish = 'INSERT, UPDATE');
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Verify that your chosen tables are in the publication.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql-stream=# SELECT * FROM pg_publication_tables WHERE pubname='pub';
Output
pubname | schemaname | tablename
---------+------------+-----------
pub | public | table1
pub | public | table2
pub | public | table3
(3 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since then, our publication &lt;code&gt;pub&lt;/code&gt; tracks all tables' changes in the &lt;code&gt;psql-stream&lt;/code&gt; database.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Let's create an abstract table &lt;code&gt;t&lt;/code&gt; and fill it with some records.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create table t (id int, name text);
INSERT INTO t(id) SELECT g.id FROM generate_series(1, 10) as g(id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a result we have 10 records in the table &lt;code&gt;t&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql-stream=# SELECT count(*) FROM t;
count
-------
10
(1 row)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Finally, it is time to check if our Logical Replication works.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run the following command in the PostgreSQL console to see Postgres WAL entries.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM pg_logical_slot_get_changes('replication_slot', NULL, NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a result, you get something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;    lsn | xid | data                          
-----------+------+--------------------------------------------------------
 0/19EA2C0 | 1045 | BEGIN 1045
 0/19EA2C0 | 1045 | table public.t: INSERT: id[integer]:1 name[text]:null
 0/19EA300 | 1045 | table public.t: INSERT: id[integer]:2 name[text]:null
 0/19EA340 | 1045 | table public.t: INSERT: id[integer]:3 name[text]:null
 0/19EA380 | 1045 | table public.t: INSERT: id[integer]:4 name[text]:null
 0/19EA3C0 | 1045 | table public.t: INSERT: id[integer]:5 name[text]:null
 0/19EA400 | 1045 | table public.t: INSERT: id[integer]:6 name[text]:null
 0/19EA440 | 1045 | table public.t: INSERT: id[integer]:7 name[text]:null
 0/19EA480 | 1045 | table public.t: INSERT: id[integer]:8 name[text]:null
 0/19EA4C0 | 1045 | table public.t: INSERT: id[integer]:9 name[text]:null
 0/19EA500 | 1045 | table public.t: INSERT: id[integer]:10 name[text]:null
 0/19EA5B0 | 1045 | COMMIT 1045
(13 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pg_logical_slot_peek_changes&lt;/code&gt; is another PostgreSQL command to peek changes from WAL entries  without consuming them.  So calling &lt;code&gt;pg_logical_slot_peek_changes&lt;/code&gt; multiple times returns the same result each time.&lt;/p&gt;
&lt;p&gt;On the other hand  &lt;code&gt;pg_logical_slot_get_changes&lt;/code&gt;  only returns results the first time. The following calls of &lt;code&gt;pg_logical_slot_get_changes&lt;/code&gt; return empty result sets. This means when &lt;code&gt;get&lt;/code&gt; command is executed, the results are served and deleted, which greatly enhancing our ability to write the logic for using these events to create a replica of the table.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Remember to destroy a slot you no longer need to stop it consuming&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT pg_drop_replication_slot('replication_slot');
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;output-plugins-&quot;&gt;Output plugins.&lt;/h4&gt;
&lt;p&gt;We've already talked about &lt;code&gt;test_decoding&lt;/code&gt; output plugin available on Postgres 9.4+. Though created as an example of an output plugin, it is still useful if your consumer supports it.&lt;/p&gt;
&lt;p&gt;Along with the &lt;code&gt;test_decoding&lt;/code&gt; plugin, another &lt;code&gt;pgoutput&lt;/code&gt; plugin is shipped with PostgreSQL natively. &lt;code&gt;pgoutput&lt;/code&gt; is available since Postgres 10. Some consumers support it for decoding (e.g. Debezium).&lt;/p&gt;
&lt;p&gt;Run the following command to create the plugin based on &lt;code&gt;pgoutput&lt;/code&gt; as in step 2 above.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM pg_create_logical_replication_slot('replication_slot', 'pgoutput');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following command consumes data changes similar to those described in step 6.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql-stream=# SELECT * FROM pg_logical_slot_peek_binary_changes('replication_slot', null, null, 'proto_version', '1', 'publication_names', 'pub');
    lsn | xid | data                                           
-----------+------+------------------------------------------------------------------------------------------
 0/19A15F8 | 1038 | \x4200000000019a1d9000027de20a91a0ea0000040e
 0/19A15F8 | 1038 | \x52000080387075626c69630074006400020169640000000017ffffffff006e616d650000000019ffffffff
 0/19A15F8 | 1038 | \x49000080384e0002740000000234306e
 0/19A1890 | 1038 | \x49000080384e0002740000000234316e
 0/19A1910 | 1038 | \x49000080384e0002740000000234326e
 0/19A1990 | 1038 | \x49000080384e0002740000000234336e
 0/19A1A10 | 1038 | \x49000080384e0002740000000234346e
 0/19A1A90 | 1038 | \x49000080384e0002740000000234356e
 0/19A1B10 | 1038 | \x49000080384e0002740000000234366e
 0/19A1B90 | 1038 | \x49000080384e0002740000000234376e
 0/19A1C10 | 1038 | \x49000080384e0002740000000234386e
 0/19A1C90 | 1038 | \x49000080384e0002740000000234396e
 0/19A1DC0 | 1038 | \x430000000000019a1d9000000000019a1dc000027de20a91a0ea
(13 rows)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here you can notice that the results are returned in binary format. &lt;em&gt;&lt;code&gt;pgoutput&lt;/code&gt; plugin produces binary output.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wal2json&lt;/code&gt; is another popular output plugin for logical decoding.&lt;/p&gt;
&lt;p&gt;Here is a sample output from &lt;code&gt;wal2json&lt;/code&gt; plugin&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
      &quot;change&quot;:[
         {
            &quot;kind&quot;:&quot;insert&quot;,
            &quot;schema&quot;:&quot;public&quot;,
            &quot;table&quot;:&quot;t&quot;,
            &quot;columnnames&quot;:[
               &quot;id&quot;,
               &quot;name&quot;
            ],
            &quot;columntypes&quot;:[
               &quot;integer&quot;,
               &quot;character varying(255)&quot;
            ],
            &quot;columnvalues&quot;:[
               1,
               &quot;&quot;
            ]
         }
      ]
   }
   {
      &quot;change&quot;:[
         {
            &quot;kind&quot;:&quot;update&quot;,
            &quot;schema&quot;:&quot;public&quot;,
            &quot;table&quot;:&quot;t&quot;,
            &quot;columnnames&quot;:[
               &quot;id&quot;,
               &quot;name&quot;
            ],
            &quot;columntypes&quot;:[
               &quot;integer&quot;,
               &quot;character varying(255)&quot;
            ],
            &quot;columnvalues&quot;:[
               1,
               &quot;New Value&quot;
            ],
            &quot;oldkeys&quot;:{
               &quot;keynames&quot;:[
                  &quot;id&quot;
               ],
               &quot;keytypes&quot;:[
                  &quot;integer&quot;
               ],
               &quot;keyvalues&quot;:[
                  1
               ]
            }
         }
      ]
   }
   {
      &quot;change&quot;:[
         {
            &quot;kind&quot;:&quot;delete&quot;,
            &quot;schema&quot;:&quot;public&quot;,
            &quot;table&quot;:&quot;t&quot;,
            &quot;oldkeys&quot;:{
               &quot;keynames&quot;:[
                  &quot;id&quot;
               ],
               &quot;keytypes&quot;:[
                  &quot;integer&quot;
               ],
               &quot;keyvalues&quot;:[
                  1
               ]
            }
         }
      ]
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;important-tips-about-slots-&quot;&gt;Important tips about slots.&lt;/h4&gt;
&lt;p&gt;Keep the following in mind when working with slots:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each slot has only one output plugin (you choose which one).&lt;/li&gt;
&lt;li&gt;Each slot provides changes from only one database.&lt;/li&gt;
&lt;li&gt;One database can have multiple slots.&lt;/li&gt;
&lt;li&gt;Each data change is typically is emitted once per slot.&lt;/li&gt;
&lt;li&gt;But a slot may re-emit the changes when the Postgres instance is restarted. A consumer must deal with this situation.&lt;/li&gt;
&lt;li&gt;An unconsumed slot is a threat to the availability of your Postgres instance. Postgres will save all WAL files for these unconsumed changes. This can lead to storage overflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;postgresql-wal-consumers-&quot;&gt;PostgreSQL WAL consumers.&lt;/h4&gt;
&lt;p&gt;A consumer is any application that can ingest the Postgres logical decoding stream.  &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/app-pgrecvlogical.html&quot;&gt;pg_recvlogical&lt;/a&gt; is a PostgreSQL application that can manage slots and consume the stream from them. It is included in the Postgres distribution,  so it's probably already installed with PostgreSQL.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?crop=entropy&amp;amp;cs=tinysrgb&amp;amp;fit=max&amp;amp;fm=jpg&amp;amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDE4fHxnbyUyMGNvZGV8ZW58MHx8fHwxNjQ4NDU1NDI3&amp;amp;ixlib=rb-1.2.1&amp;amp;q=80&amp;amp;w=2000&quot; alt=&quot;PostgreSQL Change data capture (CDC)&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Markus Spiske / Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;golang-sample-code-&quot;&gt;Golang sample code.&lt;/h4&gt;
&lt;p&gt;The following Golang code example shows how to get started creating your own Postgress WAL consumer. It uses PostgreSQL-10.x logical replication to stream database changes (decoded WAL messages) from the source database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
&quot;context&quot;
&quot;fmt&quot;
&quot;os&quot;
&quot;os/signal&quot;
&quot;strings&quot;
&quot;time&quot;

&quot;github.com/jackc/pgconn&quot;
&quot;github.com/jackc/pglogrepl&quot;
&quot;github.com/jackc/pgproto3/v2&quot;
)

const CONN = &quot;postgres://postgres:postgres@localhost/psql-streamer?replication=database&quot;
const SLOT_NAME = &quot;replication_slot&quot;
const OUTPUT_PLUGIN = &quot;pgoutput&quot;
const INSERT_TEMPLATE = &quot;create table t (id int, name text);&quot;

var Event = struct {
Relation string
Columns []string
}{}

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
conn, err := pgconn.Connect(ctx, CONN)
if err != nil {
panic(err)
}
defer conn.Close(ctx)

// 1. Create table
if _, err := conn.Exec(ctx, INSERT_TEMPLATE).ReadAll(); err != nil {
fmt.Errorf(&quot;failed to create table: %v&quot;, err)
}

// 2. ensure publication exists
if _, err := conn.Exec(ctx, &quot;DROP PUBLICATION IF EXISTS pub;&quot;).ReadAll(); err != nil {
fmt.Errorf(&quot;failed to drop publication: %v&quot;, err)
}

if _, err := conn.Exec(ctx, &quot;CREATE PUBLICATION pub FOR ALL TABLES;&quot;).ReadAll(); err != nil {
fmt.Errorf(&quot;failed to create publication: %v&quot;, err)
}

// 3. create temproary replication slot server
if _, err = pglogrepl.CreateReplicationSlot(ctx, conn, SLOT_NAME, OUTPUT_PLUGIN, pglogrepl.CreateReplicationSlotOptions{Temporary: true}); err != nil {
fmt.Errorf(&quot;failed to create a replication slot: %v&quot;, err)
}

var msgPointer pglogrepl.LSN
pluginArguments := []string{&quot;proto_version '1'&quot;, &quot;publication_names 'pub'&quot;}

// 4. establish connection
err = pglogrepl.StartReplication(ctx, conn, SLOT_NAME, msgPointer, pglogrepl.StartReplicationOptions{PluginArgs: pluginArguments})
if err != nil {
fmt.Errorf(&quot;failed to establish start replication: %v&quot;, err)
}

var pingTime time.Time
for ctx.Err() != context.Canceled {
if time.Now().After(pingTime) {
if err = pglogrepl.SendStandbyStatusUpdate(ctx, conn, pglogrepl.StandbyStatusUpdate{WALWritePosition: msgPointer}); err != nil {
fmt.Errorf(&quot;failed to send standby update: %v&quot;, err)
}
pingTime = time.Now().Add(10 * time.Second)
//fmt.Println(&quot;client: please standby&quot;)
}

ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()

msg, err := conn.ReceiveMessage(ctx)
if pgconn.Timeout(err) {
continue
}
if err != nil {
fmt.Errorf(&quot;something went wrong while listening for message: %v&quot;, err)
}

switch msg := msg.(type) {
case *pgproto3.CopyData:
switch msg.Data[0] {
case pglogrepl.PrimaryKeepaliveMessageByteID:
//fmt.Println(&quot;server: confirmed standby&quot;)

case pglogrepl.XLogDataByteID:
walLog, err := pglogrepl.ParseXLogData(msg.Data[1:])
if err != nil {
fmt.Errorf(&quot;failed to parse logical WAL log: %v&quot;, err)
}

var msg pglogrepl.Message
if msg, err = pglogrepl.Parse(walLog.WALData); err != nil {
fmt.Errorf(&quot;failed to parse logical replication message: %v&quot;, err)
}
switch m := msg.(type) {
case *pglogrepl.RelationMessage:
Event.Columns = []string{}
for _, col := range m.Columns {
Event.Columns = append(Event.Columns, col.Name)
}
Event.Relation = m.RelationName
case *pglogrepl.InsertMessage:
var sb strings.Builder
sb.WriteString(fmt.Sprintf(&quot;INSERT %s(&quot;, Event.Relation))
for i := 0; i &amp;lt; len(Event.Columns); i++ {
sb.WriteString(fmt.Sprintf(&quot;%s: %s &quot;, Event.Columns[i], string(m.Tuple.Columns[i].Data)))
}
sb.WriteString(&quot;)&quot;)
fmt.Println(sb.String())
case *pglogrepl.UpdateMessage:
var sb strings.Builder
sb.WriteString(fmt.Sprintf(&quot;UPDATE %s(&quot;, Event.Relation))
for i := 0; i &amp;lt; len(Event.Columns); i++ {
sb.WriteString(fmt.Sprintf(&quot;%s: %s &quot;, Event.Columns[i], string(m.NewTuple.Columns[i].Data)))
}
sb.WriteString(&quot;)&quot;)
fmt.Println(sb.String())
case *pglogrepl.DeleteMessage:
var sb strings.Builder
sb.WriteString(fmt.Sprintf(&quot;DELETE %s(&quot;, Event.Relation))
for i := 0; i &amp;lt; len(Event.Columns); i++ {
sb.WriteString(fmt.Sprintf(&quot;%s: %s &quot;, Event.Columns[i], string(m.OldTuple.Columns[i].Data)))
}
sb.WriteString(&quot;)&quot;)
fmt.Println(sb.String())
case *pglogrepl.TruncateMessage:
fmt.Println(&quot;ALL GONE (TRUNCATE)&quot;)
}
}
default:
fmt.Printf(&quot;received unexpected message: %T&quot;, msg)
}
}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code just logs incoming events, but in a production environment you can easily send them to a message queue or a target database.&lt;/p&gt;
&lt;h4 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;Logical decoding in PostgreSQL provides an efficient way for other application components to stay up-to-date with data changes in your Postgres database.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Traditionally, the &lt;em&gt; &lt;strong&gt;pull notification model&lt;/strong&gt; &lt;/em&gt; has been used, in which each application component queries Postgres at a certain interval. Logical encoding uses the &lt;em&gt; &lt;strong&gt;push notification model&lt;/strong&gt; &lt;/em&gt;, where Postgres notifies other parts of the application of every change as soon as it happens.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Data change events can now be sent to consumers in milliseconds without querying the database. With logic decoding, the PostgreSQL database becomes a central part of your modern dynamic real-time application.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://streams.dbconvert.com&quot;&gt;DBConvert Streams&lt;/a&gt; is a new data replication platform that uses CDC technology to stream data between databases.&lt;/p&gt;
</description>
<category>Replication</category>
<guid isPermaLink="true">https://coderlegion.com/1788/postgresql-change-data-capture-cdc-golang-sample-code</guid>
<pubDate>Thu, 03 Apr 2025 14:53:53 +0000</pubDate>
</item>
<item>
<title>Database Interaction using Python: Introduction to SQLite/MySQL/PostgreSQL</title>
<link>https://coderlegion.com/1586/database-interaction-using-python-introduction-to-sqlite-mysql-postgresql</link>
<description>&lt;p&gt;The article covers POSTGRESQL, MySQL, and SQLITE3—three major database management systems. They provide unique features and benefits and are essential to software development. Let's explore their features so that you can make better selections that will improve the project. Together, let's explore these essential elements in modern software engineering.&lt;/p&gt;
&lt;p&gt;As SQLite is &lt;strong&gt;lightweight&lt;/strong&gt;, it is a great choice for embedded applications and situations where simplicity is essential. On the other hand, MySQL stands out thanks to its strong&lt;strong&gt; client-server design&lt;/strong&gt;, &lt;strong&gt;scalability&lt;/strong&gt;, and widespread industry acceptance. PostgreSQL, on the other hand, is a popular option for&lt;strong&gt; enterprise-level &lt;/strong&gt;applications due to its &lt;strong&gt;versatility&lt;/strong&gt;, &lt;strong&gt;dependability&lt;/strong&gt;, and compliance with ACID short for Atomicity, Consistency, Isolation, and Durability.&lt;/p&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;SQLite Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;SQLite is a relational database engine that is lightweight, serverless, self-contained, and requires no configuration. Because it works directly with files, it's perfect for low-resource applications. Distribution and deployment are made easier by the fact that its self-contained databases are kept as single files. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embedded in applications&lt;/strong&gt;: SQLite, which is frequently integrated into programmes, allows internal data administration without the need for external servers. Data management is made easier by this, especially for offline functioning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File-based database&lt;/strong&gt;: SQLite databases are manageable and portable since they are kept as individual files on the file system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Appropriate for small to medium-sized applications&lt;/strong&gt;: Because of its simplicity and lightweight design, SQLite is especially well-suited for small to medium-sized applications, even though it can manage huge datasets as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's go through an example of defining tables, installing SQLite in Python, and carrying out CRUD (Create, Read, Update, Delete) operations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;conn = sqlite3.connect('example.db')&lt;br&gt;
cursor = conn.cursor()&lt;br&gt;
cursor.execute('''CREATE TABLE IF NOT EXISTS users&lt;br&gt;
                  (id INTEGER PRIMARY KEY, name TEXT, email TEXT)''')&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;, ('Alice', '*Emails are not allowed*'))&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;, ('Ali', '*Emails are not allowed*'))&lt;br&gt;
print(&quot;Records after initial insertion:&quot;)&lt;br&gt;
cursor.execute(&quot;SELECT DISTINCT * FROM users&quot;)&lt;br&gt;
rows = cursor.fetchall()&lt;br&gt;
for row in rows:&lt;br&gt;
    print(row)&lt;br&gt;
cursor.execute(&quot;UPDATE users SET email = ? WHERE name = ?&quot;, ('*Emails are not allowed*', 'Alice'))&lt;br&gt;
print(&quot;\nRecords after updating email of Alice:&quot;)&lt;br&gt;
cursor.execute(&quot;SELECT DISTINCT * FROM users&quot;)&lt;br&gt;
rows = cursor.fetchall()&lt;br&gt;
for row in rows:&lt;br&gt;
    print(row)&lt;br&gt;
cursor.execute(&quot;DELETE FROM users WHERE name = ?&quot;, ('Alice',))&lt;br&gt;
print(&quot;\nRecords after deleting Alice:&quot;)&lt;br&gt;
cursor.execute(&quot;SELECT DISTINCT * FROM users&quot;)&lt;br&gt;
rows = cursor.fetchall()&lt;br&gt;
for row in rows:&lt;br&gt;
    print(row)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=8245787205156792733&quot; alt=&quot;&quot; width=&quot;600&quot; height=&quot;250&quot;&gt;&lt;/p&gt;
&lt;div class=&quot;div-green&quot;&gt;&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt; Make sure the &lt;code&gt;'DISTINCT'&lt;/code&gt; keyword is applied to the right columns in SQL queries to obtain unique value combinations and prevent accidental duplicate results.&lt;/div&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;MySQL Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The popular open-source RDBMS MySQL provides strong features and dependability for systems ranging from small-scale initiatives to enterprise-level setups. It offers a wide range of tools and features for effective structured data management.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client-server architecture:&lt;/strong&gt;MySQL has a client-server design, which enables several clients to connect to the database server at once. The management of database resources, scalability, and security are all improved by this architecture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance and scalability:&lt;/strong&gt; MySQL is designed to be scalable, meaning it can manage big databases and high transaction volumes while maintaining optimal performance. Performance is improved by its effective caching, query optimisation, and indexing methods.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broad industrial adoption:&lt;/strong&gt; MySQL is widely used in a number of industries, including telecommunications, e-commerce, technology, and finance. Its widespread community support, dependability, and open-source nature have all added to its appeal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can get the MySQL Community Server package from &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.mysql.com/downloads/mysql/&quot;&gt; mysql official website&lt;/a&gt; and install MySQL by following the given steps. As an alternative, you can use package managers such as Homebrew for macOS or apt-get for Linux.&lt;/p&gt;
&lt;p&gt;Connecting to MySQL from Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import mysql.connector&lt;br&gt;
conn = mysql.connector.connect(user='user', password='password',&lt;br&gt;
                               host='localhost',&lt;br&gt;
                               database='test')&lt;br&gt;
cursor = conn.cursor()&lt;br&gt;
cursor.execute(&quot;SELECT * FROM table_name&quot;)&lt;br&gt;
results = cursor.fetchall()&lt;br&gt;
conn.close()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating tables and executing queries using Python's MySQL Connector:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;cursor = conn.cursor()&lt;br&gt;
cursor.execute('''CREATE TABLE IF NOT EXISTS users&lt;br&gt;
                  (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))''')&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (%s, %s)&quot;, ('Alice', '*Emails are not allowed*'))&lt;br&gt;
cursor.execute(&quot;SELECT * FROM users&quot;)&lt;br&gt;
results = cursor.fetchall()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=4398450430378255567&quot; alt=&quot;&quot; width=&quot;650&quot; height=&quot;400&quot;&gt;&lt;/p&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;PostgreSQL Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Strong and open-source, PostgreSQL is an &lt;strong&gt;object-relational database system&lt;/strong&gt; that is well-known for its feature-rich set, dependability, and extensibility. It is a top option for both small-scale applications and large corporate systems because of its comprehensive functions that enable complicated data types, transactions, and data integrity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Extensibility&lt;/strong&gt;: Because of PostgreSQL's flexible architecture, programmers are able to design unique functions, data types, and indexing techniques to meet the needs of particular applications. The smooth integration of third-party extensions and libraries is made possible by this flexibility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACID Compliance&lt;/strong&gt;: PostgreSQL guarantees ACID (&lt;br&gt;&lt;strong&gt;Atomicity&lt;/strong&gt;: To preserve database integrity, transactions are either fully completed or entirely aborted.&lt;br&gt;&lt;strong&gt;Consistency&lt;/strong&gt;: Assures that the database is kept intact by writing only legitimate data.&lt;br&gt;&lt;strong&gt;Isolation&lt;/strong&gt;: Data integrity is ensured by many transactions running simultaneously without interfering.&lt;br&gt;&lt;strong&gt;Durability&lt;/strong&gt;: Even in the event of a system failure, committed transaction modifications remain intact.)  compliance, which in turn provides dependable and consistent database transactions. This functionality is crucial for ensuring consistency across the database and data integrity even in the case of system failures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Integrity and Reliability&lt;/strong&gt;: PostgreSQL uses built-in restrictions like triggers and rules, foreign keys, and concurrency management to guarantee data integrity and stability.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;p&gt;To install PostgreSQL, you can download and install the PostgreSQL package from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/download/&quot;&gt; official website&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import psycopg2&lt;br&gt;
conn = psycopg2.connect(&lt;br&gt;
    dbname='your_database_name',&lt;br&gt;
    user='your_username',&lt;br&gt;
    password='your_password',&lt;br&gt;
    host='localhost',&lt;br&gt;
    port='5432'&lt;br&gt;
)&lt;br&gt;
if conn:&lt;br&gt;
    print(&quot;Connected to PostgreSQL&quot;)&lt;br&gt;
else:&lt;br&gt;
    print(&quot;Failed to connect to PostgreSQL&quot;)&lt;br&gt;
conn.close()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating tables and executing queries using Python's psycopg2 library:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;# Create a cursor object&lt;br&gt;
cursor = conn.cursor()&lt;br&gt;
cursor.execute('''CREATE TABLE IF NOT EXISTS users (&lt;br&gt;
                    id SERIAL PRIMARY KEY,&lt;br&gt;
                    name VARCHAR(255),&lt;br&gt;
                    email VARCHAR(255)&lt;br&gt;
                )''')&lt;br&gt;
cursor.execute(&quot;INSERT INTO users (name, email) VALUES (%s, %s)&quot;, ('Alice', '*Emails are not allowed*'))&lt;br&gt;
cursor.execute(&quot;SELECT * FROM users&quot;)&lt;br&gt;
results = cursor.fetchall()&lt;br&gt;
for row in results:&lt;br&gt;
    print(row)&lt;br&gt;
conn.commit()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;div-blue&quot;&gt;&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt; Always securely handle database credentials and avoid hardcoding them directly into your code for better security.&lt;/div&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Comparison&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;table border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Good for &lt;br&gt;small to medium&lt;br&gt;-sized applications&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;Optimized for &lt;br&gt;read-heavy &lt;br&gt;workloads&lt;/td&gt;
&lt;td&gt;Optimized for &lt;br&gt;complex &lt;br&gt;queries &lt;br&gt;and large &lt;br&gt;datasets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Limited scalability &lt;br&gt;due to file-&lt;br&gt;based architecture&lt;/td&gt;
&lt;td&gt;Good scalability &lt;br&gt;with support for &lt;br&gt;clustering and &lt;br&gt;sharding&lt;/td&gt;
&lt;td&gt;High scalability &lt;br&gt;with advanced &lt;br&gt;replication and &lt;br&gt;partitioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Features&lt;/td&gt;
&lt;td&gt;Basic features with limited concurrency support&lt;/td&gt;
&lt;td&gt;Rich feature set including stored procedures, triggers, and views&lt;/td&gt;
&lt;td&gt;Comprehensive feature set including JSONB data type, &lt;br&gt;full-text search, and &lt;br&gt;advanced indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ease of Use&lt;/td&gt;
&lt;td&gt;Simple setup and usage, suitable for beginners&lt;/td&gt;
&lt;td&gt;Easy to use, especially &lt;br&gt;with graphical tools like MySQL Workbench&lt;/td&gt;
&lt;td&gt;Requires more &lt;br&gt;configuration but &lt;br&gt;offers powerful features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use Case Suitability&lt;/td&gt;
&lt;td&gt;Ideal for &lt;br&gt;embedded &lt;br&gt;systems &lt;br&gt;and small &lt;br&gt;applications&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Well-suited for web applications &lt;br&gt;and medium-&lt;br&gt;sized databases&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;Best for large-scale applications with &lt;br&gt;complex data &lt;br&gt;requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Advanced Concepts&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Advanced ideas like indexing, optimization, and transactions are essential for improving the scalability, performance, and dependability of databases.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transactions:&lt;/strong&gt; Transactions ensure the ACID properties of database operations, handling multiple tasks as a single unit for data accuracy and reliability. All database systems provide tools for effective transaction management.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Indexing&lt;/strong&gt;: Indexes expedite data retrieval, particularly for SELECT queries, by swiftly accessing entries in database tables based on specific field values. Effective indexing reduces the need for full table scans, significantly enhancing query performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimization&lt;/strong&gt;: Database optimization strategies improve effectiveness and performance through resource usage optimization, performance tuning, query optimization, and schema design optimization. Each system provides unique features and tools to enhance productivity and address performance issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import sqlite3&lt;br&gt;
import mysql.connector&lt;br&gt;
import psycopg2&lt;br&gt;
def sqlite_transactions():&lt;br&gt;
    conn = sqlite3.connect('example.db')&lt;br&gt;
    cursor = conn.cursor()&lt;br&gt;
    try:&lt;br&gt;
        conn.execute(&quot;BEGIN&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance - 100 WHERE id = 1&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance + 100 WHERE id = 2&quot;)&lt;br&gt;
        conn.commit()&lt;br&gt;
        print(&quot;Transaction committed successfully&quot;)&lt;br&gt;
    except sqlite3.Error as e:&lt;br&gt;
        conn.rollback()&lt;br&gt;
        print(&quot;Transaction rolled back due to error:&quot;, e)&lt;br&gt;
    finally:&lt;br&gt;
        conn.close()&lt;br&gt;
def mysql_transactions():&lt;br&gt;
    conn = mysql.connector.connect(user='user', password='password', database='example')&lt;br&gt;
    cursor = conn.cursor()&lt;br&gt;
    try:&lt;br&gt;
        conn.start_transaction()&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance - 100 WHERE id = 1&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance + 100 WHERE id = 2&quot;)&lt;br&gt;
        conn.commit()&lt;br&gt;
        print(&quot;Transaction committed successfully&quot;)&lt;br&gt;
    except mysql.connector.Error as e:&lt;br&gt;
        conn.rollback()&lt;br&gt;
        print(&quot;Transaction rolled back due to error:&quot;, e)&lt;br&gt;
    finally:&lt;br&gt;
        conn.close()&lt;br&gt;
def postgres_transactions():&lt;br&gt;
    conn = psycopg2.connect(dbname='example', user='user', password='password', host='localhost')&lt;br&gt;
    cursor = conn.cursor()&lt;br&gt;
    try:&lt;br&gt;
        conn.autocommit = False&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance - 100 WHERE id = 1&quot;)&lt;br&gt;
        cursor.execute(&quot;UPDATE users SET balance = balance + 100 WHERE id = 2&quot;)&lt;br&gt;
        conn.commit()&lt;br&gt;
        print(&quot;Transaction committed successfully&quot;)&lt;br&gt;
    except psycopg2.Error as e:&lt;br&gt;
        conn.rollback()&lt;br&gt;
        print(&quot;Transaction rolled back due to error:&quot;, e)&lt;br&gt;
    finally:&lt;br&gt;
        conn.close()&lt;br&gt;
sqlite_transactions()&lt;br&gt;
mysql_transactions()&lt;br&gt;
postgres_transactions()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SQLite, designed for single-user and small-scale applications, uses local file-based databases and simple transaction management with BEGIN, COMMIT, and ROLLBACK statements. &lt;br&gt;MySQL, a robust RDBMS, offers scalability and concurrency control with start_transaction(), commit(), and rollback() methods.&lt;br&gt;PostgreSQL, known for advanced features and extensibility, provides strong support for concurrency, data integrity, and advanced SQL features, managing transactions via autocommit mode and commit()/rollback() methods.&lt;/p&gt;
&lt;/section&gt;
&lt;div class=&quot;div-faq&quot;&gt;&lt;span class=&quot;faq-header&quot;&gt;FAQ&lt;/span&gt; &lt;span class=&quot;faq-body&quot;&gt;&lt;br&gt;Q: What is the difference between SQL and NoSQL databases?&lt;br&gt;A: SQL (Structured Query Language) databases, also known as relational databases, organize data into tables with predefined schemas and use SQL for querying. NoSQL databases, on the other hand, store data in flexible, non-tabular formats and do not require a fixed schema. They are designed for scalability and handling unstructured data.&lt;/span&gt;&lt;/div&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Databases are essential to software development because they allow for effective data management. MySQL scales for the web, PostgreSQL meets enterprise needs, and SQLite is ideal for&lt;strong&gt; lightweight applications&lt;/strong&gt;. When choosing a project, it is essential to comprehend their strengths. Using &lt;strong&gt;cutting-edge ideas&lt;/strong&gt; promotes creativity in digital solutions by ensuring data performance and dependability.&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Reference&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;For more reference:&lt;br&gt;SQLite Official Website:&lt;br&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite Official Website&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
</description>
<category>Python Database Interaction</category>
<guid isPermaLink="true">https://coderlegion.com/1586/database-interaction-using-python-introduction-to-sqlite-mysql-postgresql</guid>
<pubDate>Wed, 19 Mar 2025 11:56:30 +0000</pubDate>
</item>
</channel>
</rss>