<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged prisma</title>
<link>https://coderlegion.com/tag/prisma</link>
<description>Powered by Question2Answer</description>
<item>
<title>Cancel or send: one winner. An order flow on Prisma Postgres and Compute</title>
<link>https://coderlegion.com/32897/cancel-or-send-one-winner-an-order-flow-on-prisma-postgres-and-compute</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Werbung / Disclosure:&lt;/strong&gt; Prisma gave me a coupon for two months of a paid plan for free and said it will add free credits, so I could try &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.prisma.io/postgres&quot;&gt;Prisma Postgres&lt;/a&gt; and Compute for this project. All results below come from my own runs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I built an order flow with a 10-minute grace period. Cancellation and dispatch must have exactly one winner in the database.&lt;/li&gt;
&lt;li&gt;The winner is decided in Postgres, with a guarded update, a row lock and &lt;code&gt;SKIP LOCKED&lt;/code&gt;. No clock and no timer decides it.&lt;/li&gt;
&lt;li&gt;I broke the code on purpose, one line at a time, to check that my tests really fail. One of my race tests passed even without the lock.&lt;/li&gt;
&lt;li&gt;On Compute, an idle app sleeps. My poller is a timer inside the server, so it only runs when someone calls the app. This changes the timing, not the correctness.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;/h3&gt;
&lt;p&gt;A customer orders a custom T-shirt. I promise a grace period of 10 minutes. During that time the customer can cancel. After that, the printer gets an email and the customer cannot cancel any more.&lt;/p&gt;
&lt;p&gt;Two things can happen at almost the same moment. The customer presses cancel, and my poller wants to send. Exactly one of them must win. A cancelled order must never reach the printer, because an email cannot be recalled.&lt;/p&gt;
&lt;h3 id=&quot;design-decisions&quot;&gt;Design decisions&lt;/h3&gt;
&lt;h4 id=&quot;claim-first-then-send&quot;&gt;Claim first, then send&lt;/h4&gt;
&lt;p&gt;The poller claims an order by changing its status from &lt;code&gt;PENDING&lt;/code&gt; to &lt;code&gt;SENT&lt;/code&gt; before it sends the email. If the customer cancels first, the claim finds nothing. If the claim wins, the cancel is rejected with a 409.&lt;/p&gt;
&lt;p&gt;Here, &lt;code&gt;SENT&lt;/code&gt; means the order has been claimed for dispatch, not that the email has been delivered. If the server crashes after the claim, the printer may get no email. I set &lt;code&gt;printerNotifiedAt&lt;/code&gt; only after Resend accepts it. A stuck-order query finds &lt;code&gt;SENT&lt;/code&gt; orders without that timestamp, and a resend route lets me recover them.Sending the email before claiming the order could allow it to be sent even if the cancellation wins the database race.&lt;/p&gt;
&lt;h4 id=&quot;a-guarded-transition&quot;&gt;A guarded transition&lt;/h4&gt;
&lt;p&gt;Every status change goes through one function. It checks that the transition is allowed for the actor. Then it writes with the old status in the where clause:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const { count } = await prisma.order.updateMany({
  where: { id, status: order.status },
  data: { status: to },
});
if (count === 0) throw new ConflictException('Order changed, please retry');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If someone changed the row between my read and my write, &lt;code&gt;count&lt;/code&gt; is 0 and the caller gets a 409. This is the sixth time I used this read-then-write pattern across my projects.&lt;/p&gt;
&lt;h4 id=&quot;a-postgres-poller-not-a-queue&quot;&gt;A Postgres poller, not a queue&lt;/h4&gt;
&lt;p&gt;The hosted version has no Redis, so there is no BullMQ. The order and its deadline are written in one transaction. A timer loop claims due orders with one statement:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;UPDATE &quot;Order&quot; SET status = 'SENT' -- plus updatedAt
WHERE id IN (
  SELECT id FROM &quot;Order&quot;
  WHERE status = 'PENDING' AND &quot;sendAt&quot; &amp;lt;= (now() AT TIME ZONE 'UTC')
  ORDER BY &quot;sendAt&quot; LIMIT 10
  FOR UPDATE SKIP LOCKED
)
RETURNING id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no enqueue gap, because the deadline is data. The costs are a polling delay and retry logic that I write myself. Today there is no automatic retry. The safety net is the stuck-order query and the resend route.&lt;/p&gt;
&lt;p&gt;The server owns &lt;code&gt;sendAt&lt;/code&gt;. A client-supplied value could skip the grace period, so the request schema rejects unknown keys.&lt;/p&gt;
&lt;h4 id=&quot;a-row-lock-on-the-design&quot;&gt;A row lock on the design&lt;/h4&gt;
&lt;p&gt;An order points at a design. If the design changes after the order exists, the printer prints something the customer never ordered, and nobody sees an error. Creating an order, updating a design and deleting a design all take &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the design row first. A design counts as locked when it has any order that is not cancelled.&lt;/p&gt;
&lt;h3 id=&quot;tests-and-a-test-that-proved-nothing&quot;&gt;Tests, and a test that proved nothing&lt;/h3&gt;
&lt;p&gt;I wrote integration tests against a real Postgres database. Then I broke the code on purpose, one line at a time, and checked that a test turned red.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;I removed &lt;/th&gt;&lt;th&gt;What went red&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;the status in the &lt;code&gt;updateMany&lt;/code&gt; guard &lt;/td&gt;&lt;td&gt;the cancel-versus-poller test, and all 30 printer/admin confirm iterations&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;status = 'PENDING'&lt;/code&gt; in the claim &lt;/td&gt;&lt;td&gt;a cancelled order was emailed, and two pollers sent two emails&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;SKIP LOCKED&lt;/code&gt; &lt;/td&gt;&lt;td&gt;the locked-row test (the pollers waited)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;FOR UPDATE&lt;/code&gt; in order creation &lt;/td&gt;&lt;td&gt;only the deterministic lock test&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;FOR UPDATE&lt;/code&gt; in design update &lt;/td&gt;&lt;td&gt;only the deterministic lock test (65 passed, 1 failed)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;FOR UPDATE&lt;/code&gt; in design removal &lt;/td&gt;&lt;td&gt;the remove-versus-create test: a foreign key error instead of a 409&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The lock rows are the lesson. First I tested the lock with 30 free-running iterations of update versus create. They all passed with the lock removed. The broken interleaving ends in the same final database state as the harmless one, so no assertion on the final state can tell them apart. Only a test that pauses order creation after it takes the lock, and then checks whether the update can finish, sees the bug.&lt;/p&gt;
&lt;p&gt;A test also found a timezone bug. I ran the integration session in America/Los_Angeles. Comparing sendAt with a bare now() failed because sendAt is a timestamp without time zone. The comparison now uses now() AT TIME ZONE 'UTC', assuming sendAt is stored as a UTC timestamp. This fixes the timezone interpretation, but not clock skew between the application and database; I discuss that remaining limitation below.&lt;/p&gt;
&lt;h3 id=&quot;prisma-postgres-and-compute&quot;&gt;Prisma Postgres and Compute&lt;/h3&gt;
&lt;p&gt;Prisma ORM 8 (I write Prisma 8 for short) is a release candidate and Composer is in Early Access. Compute has been generally available since August 2026. Everything in this section was true on my machine in October 2026. Versions: Prisma CLI 8.0.0-rc.27, &lt;code&gt;@prisma/orm-postgres&lt;/code&gt; 8.0.0-rc.20, Composer 0.34.0.&lt;/p&gt;
&lt;h4 id=&quot;why-prisma-orm-8-is-in-this-project&quot;&gt;Why Prisma ORM 8 is in this project&lt;/h4&gt;
&lt;p&gt;The Console told me that my repository used &lt;code&gt;prisma ^7.7.0&lt;/code&gt;, that deploys run on Prisma 8, and that I should upgrade first. So I installed Prisma 8 next to Prisma 7, inferred a contract from my database, and planned a baseline migration. The plan had 15 operations from an empty database: four enums, four tables, the &lt;code&gt;(status, sendAt)&lt;/code&gt; index and four foreign keys.&lt;/p&gt;
&lt;p&gt;Later, Martin from Prisma told me that a Prisma 7 app runs fine on Compute and that Prisma 8 is not required. I did not test a Prisma 7 only deploy myself, so I cannot confirm it, and I reported the Console banner to Prisma.&lt;/p&gt;
&lt;p&gt;I kept Prisma 8 for one job: the deploy applies my baseline migration to the new database. My queries still run on Prisma 7 with the &lt;code&gt;pg&lt;/code&gt; adapter. The docs also describe a plainer option, &lt;code&gt;rawPostgres()&lt;/code&gt;, which hands your own client a &lt;code&gt;db.url&lt;/code&gt;. With it, you migrate the database yourself. I did not use it.&lt;/p&gt;
&lt;p&gt;I kept the queries on Prisma 7 for another reason. The release status page lists what Prisma 8 does not have yet, for example transaction isolation levels, atomic increment, most nested writes and &lt;code&gt;P2002&lt;/code&gt;-style error codes. My locks use raw SQL and a guarded &lt;code&gt;updateMany&lt;/code&gt;, and I did not try them on Prisma 8.&lt;/p&gt;
&lt;p&gt;Two more facts for Prisma 7 users. First, &lt;code&gt;npx prisma&lt;/code&gt; now runs the Prisma 8 CLI, which has different data commands (&lt;code&gt;contract&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;migration&lt;/code&gt;). Pin &lt;code&gt;prisma&lt;/code&gt; to &lt;code&gt;^7&lt;/code&gt; and use &lt;code&gt;npx prisma@7&lt;/code&gt;, or install an alias package like I did. Second, Prisma 8 is a release candidate, and the release status page expects general availability in October 2026.&lt;/p&gt;
&lt;h4 id=&quot;getting-a-nestjs-11-app-onto-compute&quot;&gt;Getting a NestJS 11 app onto Compute&lt;/h4&gt;
&lt;p&gt;Compute runs a single bundled file. My notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The deploy tool refused Node 22.23.4 and asked for Node 24.11.1 or newer. The Compute docs say Node 22.18 or newer on the 22 line is enough, so the docs and the tool disagree.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bcrypt&lt;/code&gt; is a native addon. Bundled into one file, it crashed at startup with &lt;code&gt;__dirname is not defined&lt;/code&gt;. I switched to &lt;code&gt;bcryptjs&lt;/code&gt;. Login and the tests still passed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prisma dev&lt;/code&gt; is not supported on Windows yet, so I tested the bundle with plain &lt;code&gt;node&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Composer needs &lt;code&gt;type: module&lt;/code&gt;. I put the Composer files in a &lt;code&gt;deploy&lt;/code&gt; folder with its own &lt;code&gt;package.json&lt;/code&gt;, so &lt;code&gt;nest build&lt;/code&gt; stays CommonJS. A small launcher reads the database URL that the platform provides and then starts the compiled app.&lt;/li&gt;
&lt;li&gt;The Console's setup pull request failed at &lt;code&gt;npm install&lt;/code&gt; with &lt;code&gt;ERESOLVE&lt;/code&gt;. I could reproduce a conflict on my machine by adding &lt;code&gt;alchemy&lt;/code&gt; next to Jest 30 (a Babel peer dependency clash). I could not see the Console's log, so I do not know if it is the same cause.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first deploy created ten resources, including the database and the migration step, and returned a public URL. The docs say Compute runs deployed apps on Bun, and I built my bundle for Node. The app worked when I tried it. The poller is switched off on that deployment, so orders stay &lt;code&gt;PENDING&lt;/code&gt; there.&lt;/p&gt;
&lt;h3 id=&quot;the-surprise-scale-to-zero-versus-my-poller&quot;&gt;The surprise: scale to zero versus my poller&lt;/h3&gt;
&lt;p&gt;My poller is a timer inside the server. I deployed a small test app with a counter that ticks every 5 seconds and shows its history. Then I left it alone overnight.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;startedAt&lt;/code&gt; value was the same after 15 hours, so the app was never restarted.&lt;/li&gt;
&lt;li&gt;Only 2 ticks were recorded. The first gap was 54,940 seconds.&lt;/li&gt;
&lt;li&gt;New ticks appeared at the moments I opened the page. Then they stopped again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Compute docs describe this: idle instances sleep with a memory snapshot and resume in milliseconds. A timer inside the process only runs when someone calls the app.&lt;/p&gt;
&lt;p&gt;What still holds: one database claim wins, and a cancelled order cannot be claimed for dispatch through this flow. What breaks is the timing. The email goes out after the next request, not at minute 10. Exactly-once email delivery is not guaranteed; a crash during recovery can result in a duplicate.&lt;/p&gt;
&lt;h4 id=&quot;open-question-where-should-the-poller-run-&quot;&gt;Open question: where should the poller run?&lt;/h4&gt;
&lt;p&gt;Compute has no built-in cron scheduler. A Prisma engineer told me that the recommended patterns are options 2 and 3 below, and that keep-awake is meant for short, bounded work. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.prisma.io/docs/compute/limitations&quot;&gt;Compute limitations&lt;/a&gt; page and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.prisma.io/docs/composer/building-blocks#cron&quot;&gt;Composer cron building block&lt;/a&gt; describe both. I have not chosen between them yet, and I would like to discuss it. These are the options I found:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep the instance awake.&lt;/strong&gt; Prisma says keep-awake is meant for short, bounded work, not for an always-on loop. The docs add that it does not guarantee continuity through restarts or deployments. I have not measured what a permanently awake instance costs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An external trigger (recommended by Prisma).&lt;/strong&gt; A scheduler outside Compute calls a protected route. The route wakes the app, runs the same claim logic and returns. My tests show that extra triggers are safe: two pollers claim an order once. The timing precision depends on the scheduler, and the request must start responding within 60 seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Composer cron building block (recommended by Prisma).&lt;/strong&gt; Its scheduler is a service that never scales to zero, so it costs one warm instance. Composer is in Early Access, and I did not find an example of mounting the cron runner in a NestJS app.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Questions that are still open:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the always-warm cron scheduler count against the Free plan, and how much memory does it use?&lt;/li&gt;
&lt;li&gt;Has anyone mounted the cron runner inside a NestJS app?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you solved this, I would like to hear how.&lt;/p&gt;
&lt;h3 id=&quot;raw-output&quot;&gt;Raw output&lt;/h3&gt;
&lt;p&gt;These are trimmed outputs from my own runs.&lt;/p&gt;
&lt;p&gt;The deploy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;t-shirt-backend-test
├─ database   postgres-database db_...
└─ app        compute-service cps_...
Stack deployed (10/10)
Deployed t-shirt-backend-test to production in 3m 1s.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The bundle with the native &lt;code&gt;bcrypt&lt;/code&gt; addon, at startup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ReferenceError: __dirname is not defined in ES module scope
    at ... require_node_gyp_build()(path.resolve(__dirname))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test app's ticks, first call (15 hours after the deploy):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&quot;startedAt&quot;:&quot;2026-10-25T16:36:23.113Z&quot;,&quot;now&quot;:&quot;2026-10-26T07:59:53.581Z&quot;,&quot;count&quot;:2,
 &quot;gaps&quot;:[{&quot;from&quot;:&quot;2026-10-25T16:42:26.838Z&quot;,&quot;to&quot;:&quot;2026-10-26T07:58:06.462Z&quot;,&quot;seconds&quot;:54940}]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second call, about 10 minutes later (the first gap is shortened):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&quot;startedAt&quot;:&quot;2026-10-25T16:36:23.113Z&quot;,&quot;now&quot;:&quot;2026-10-26T08:10:05.860Z&quot;,&quot;count&quot;:4,
 &quot;gaps&quot;:[{&quot;seconds&quot;:54940},{&quot;from&quot;:&quot;2026-10-26T07:58:06.462Z&quot;,&quot;to&quot;:&quot;2026-10-26T07:59:53.591Z&quot;,&quot;seconds&quot;:107}]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two failures from the mutation runs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;status guard removed, confirm race:   Expected length: 1   Received length: 2
design lock removed, lock-held test:  Expected: &quot;rejected&quot;  Received: &quot;fulfilled&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-i-did-not-verify&quot;&gt;What I did not verify&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I did not run the poller on Compute under load, and I did not measure how late the emails are.&lt;/li&gt;
&lt;li&gt;I have not fixed clock skew between the app server and the database. &lt;code&gt;sendAt&lt;/code&gt; is written from the app clock and compared with the database clock, so skew changes the grace period silently.&lt;/li&gt;
&lt;li&gt;There is no automatic retry for failed emails.&lt;/li&gt;
&lt;li&gt;I did not port locks and transactions to Prisma 8. They stay on Prisma 7.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Let the database decide the winner. Do not let a timer or a clock decide it.&lt;/li&gt;
&lt;li&gt;Break your code on purpose. A test that also passes without the guard proves nothing.&lt;/li&gt;
&lt;li&gt;A free-running race loop can pass for the wrong reason. Force the interleaving.&lt;/li&gt;
&lt;li&gt;Scale to zero changes what a timer inside your server means. Check it before you design around it.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;I'm Mhamd, a backend engineer working with NestJS and TypeScript. Portfolio: &lt;a rel=&quot;nofollow&quot; href=&quot;https://mhamd021.github.io&quot;&gt;mhamd021.github.io&lt;/a&gt; · GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Mhamd021&quot;&gt;Mhamd021&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32897/cancel-or-send-one-winner-an-order-flow-on-prisma-postgres-and-compute</guid>
<pubDate>Wed, 28 Oct 2026 21:30:34 +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>How I Survived My First Full stack App with Next.js</title>
<link>https://coderlegion.com/4131/how-i-survived-my-first-full-stack-app-with-next-js</link>
<description>&lt;p&gt;The story begins—as it always does—with a tutorial.&lt;/p&gt;
&lt;p&gt;I was watching yet another video and thought: &lt;em&gt;“Let’s build a full stack app.”&lt;/em&gt; I already had a background in React, and I’d made a few full stack projects before. So this time, I chose &lt;strong&gt;Next.js&lt;/strong&gt;. It looked easy, especially with the new App Router.&lt;/p&gt;
&lt;p&gt;Why Next.js? Because it’s React at the front, backend APIs included, and server actions baked right in. No need for separate Express servers or wiring up extra routers. Also, the assumption was simple: &lt;em&gt;everything is a server component unless you slap a &lt;code&gt;&quot;use client&quot;&lt;/code&gt; on top.&lt;/em&gt; I was in.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-why-i-fell-in-love-with-the-app-router&quot;&gt;✨ Why I Fell in Love with the App Router&lt;/h3&gt;
&lt;p&gt;One of the best things about Next.js is the App Router. You don’t need to install React Router and define routes in &lt;code&gt;App.jsx&lt;/code&gt;. Just drop a file in the &lt;code&gt;/app&lt;/code&gt; folder, and boom—it’s a route. It's simple. It’s fast. It works.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;first-battle-the-ui-fear&quot;&gt;First Battle: The UI Fear&lt;/h3&gt;
&lt;p&gt;Here’s my harsh truth—I suck at design.&lt;/p&gt;
&lt;p&gt;My pages looked &lt;em&gt;ugly&lt;/em&gt;. And that really messes with you. It triggers imposter syndrome. I’d open VS Code, freeze, and think: &lt;em&gt;What do I even build here?&lt;/em&gt; The spiral begins: “Am I even cut out for frontend dev?”&lt;/p&gt;
&lt;p&gt;Sound familiar? Been there. This time, I didn’t let it win.&lt;/p&gt;
&lt;h4 id=&quot;here-s-how-i-beat-the-block-&quot;&gt;Here's how I beat the block:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Took breaks&lt;/strong&gt; when stuck.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drew wireframes&lt;/strong&gt;, even if they were ugly. Just enough to give me direction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Used inspiration from other websites&lt;/strong&gt;. Seriously, 90% of the internet uses the same layout tricks. Why not learn from them?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools like Google Stitch, v0.dev, and Bolt&lt;/strong&gt; helped generate page ideas and inspiration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any progress is better than no progress. Remember that.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-server-actions&quot;&gt;❤️ Server Actions&lt;/h3&gt;
&lt;p&gt;God bless Server Actions.&lt;/p&gt;
&lt;p&gt;No more creating an API route or writing fetch calls and handling them manually. Need to read from the server? Just keep the page component as a server component, use a Server Action to grab the data, and pass it down to your client components. Done.&lt;/p&gt;
&lt;p&gt;You can &lt;strong&gt;use server actions inside client components too&lt;/strong&gt;, but there are caveats. I won’t pretend I know them all—this project was me figuring things out on the fly. So I won’t preach, but here’s what worked for me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep data fetching in server components.&lt;/li&gt;
&lt;li&gt;Use server actions to mutate or fetch data.&lt;/li&gt;
&lt;li&gt;If using forms, yes—you can still call server actions from client components.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-i-wrote-ugly-code-fast&quot;&gt;How I Wrote Ugly Code—Fast&lt;/h3&gt;
&lt;p&gt;Not gonna lie. I was a messy developer in the beginning.&lt;/p&gt;
&lt;p&gt;Every time I made a new page/component, I didn't care about reusability. Things got chaotic fast.&lt;/p&gt;
&lt;p&gt;But I followed &lt;strong&gt;one rule&lt;/strong&gt;: put every component inside a well-named folder. For example: &lt;code&gt;/components/dashboard/Table.tsx&lt;/code&gt;. Later, when I needed it somewhere else, I refactored it—added props, defined interfaces, added type safety—and made it reusable.&lt;/p&gt;
&lt;p&gt;So... am I a great React developer? Maybe not.&lt;/p&gt;
&lt;p&gt;Am I resourceful and fast? Yes.&lt;/p&gt;
&lt;p&gt;Best case: smart developer.&lt;/p&gt;
&lt;p&gt;Worst case: functional chaos.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-email-saga&quot;&gt;The Email Saga&lt;/h3&gt;
&lt;p&gt;Now I understand why dev’s complain about &lt;strong&gt;bad DX (Developer Experience)&lt;/strong&gt; on Twitter.&lt;/p&gt;
&lt;p&gt;Until now, I had a chill life—&lt;code&gt;React + Vite&lt;/code&gt;, everything worked like magic. But for this project, I needed to &lt;strong&gt;send emails&lt;/strong&gt; inviting users to give feedback.&lt;/p&gt;
&lt;p&gt;I initially tried &lt;strong&gt;Mailjet&lt;/strong&gt; — looked promising, solid docs, decent limits. But then... &lt;strong&gt;everything broke&lt;/strong&gt; after sending just 5 emails.&lt;/p&gt;
&lt;p&gt;I got hit with a &lt;strong&gt;&quot;temporary restriction&quot;&lt;/strong&gt; and no real explanation. Just a vague message and complete silence. I waited &lt;strong&gt;three days&lt;/strong&gt;, hoping the ban would lift. Spoiler: it didn’t.&lt;/p&gt;
&lt;p&gt;That’s when I realized how important good &lt;strong&gt;developer experience&lt;/strong&gt; really is.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If I ever build a dev tool, I’m writing great docs, clear errors, and probably a fun YouTube guide.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, I ditched Mailjet and moved to &lt;strong&gt;Resend&lt;/strong&gt; — and this time, everything &lt;em&gt;just worked&lt;/em&gt;. Fast setup, friendly DX, and no cryptic errors.&lt;/p&gt;
&lt;p&gt;❤️ Huge shoutout to Resend.&lt;/p&gt;
&lt;p&gt;???? No hard feelings, Mailjet — but that experience wasn’t it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-broken-database-horror&quot;&gt;The Broken Database Horror&lt;/h3&gt;
&lt;p&gt;After surviving all that, I wanted to &lt;strong&gt;brag on LinkedIn&lt;/strong&gt;: “Hey look! I integrated payments!” Even if they were test payments—still, it’s progress.&lt;/p&gt;
&lt;p&gt;But the horror unfolded quickly.&lt;/p&gt;
&lt;p&gt;Understanding how payments work end-to-end—from initiation to confirmation—is &lt;strong&gt;critical&lt;/strong&gt;. If you don’t, you’ll keep breaking your schema with every little change.&lt;/p&gt;
&lt;h4 id=&quot;what-i-learned-&quot;&gt;What I learned:&lt;/h4&gt;
&lt;p&gt;If you're dealing with &lt;strong&gt;third-party APIs or external systems&lt;/strong&gt;, always:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Track their &lt;strong&gt;status&lt;/strong&gt; (&lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;failure&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;enum&lt;/code&gt; fields to reflect the state&lt;/li&gt;
&lt;li&gt;Expect that some parts of the flow are &lt;strong&gt;out of your control&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Know what you're storing and when. Or prepare to suffer. And yes, it &lt;em&gt;will&lt;/em&gt; become a dev horror story.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;linting-typescript-woes&quot;&gt;Linting + TypeScript Woes&lt;/h3&gt;
&lt;p&gt;I used to think &lt;strong&gt;linting&lt;/strong&gt; was just that one VS Code extension that makes code pretty.&lt;/p&gt;
&lt;p&gt;Then I ran &lt;code&gt;npm run build&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Boom. &lt;strong&gt;10,000+ lines of errors.&lt;/strong&gt; Why? It tried to lint the auto-generated code too.&lt;/p&gt;
&lt;p&gt;So yeah, I had to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fix unused variables&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;any&lt;/code&gt; types&lt;/li&gt;
&lt;li&gt;Update configs to ignore generated folders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lesson learned: don't skip linting. TypeScript isn’t just for safety—it’s a build-saver.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;deployment-amp-production-nightmares&quot;&gt;Deployment &amp;amp; Production Nightmares&lt;/h3&gt;
&lt;p&gt;I’d spent a month building this project. Now it was time to ship. Resume-ready, you know?&lt;/p&gt;
&lt;p&gt;I used &lt;strong&gt;Vercel&lt;/strong&gt;. Guess what?&lt;/p&gt;
&lt;p&gt;Vercel &lt;strong&gt;kept trying to render server pages as static&lt;/strong&gt;. My build kept failing.&lt;/p&gt;
&lt;p&gt;Turns out: Vercel will &lt;em&gt;try to statically generate everything&lt;/em&gt; unless you tell it not to.&lt;/p&gt;
&lt;h4 id=&quot;solution-&quot;&gt;Solution:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const dynamic = &quot;force-dynamic&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Boom. Now it knows your page is meant to run &lt;strong&gt;server-side&lt;/strong&gt; only.&lt;/p&gt;
&lt;p&gt;Oh, and &lt;strong&gt;Supabase&lt;/strong&gt;—my free DB. Connecting it was hell.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There’s a &lt;strong&gt;pooled connection&lt;/strong&gt; (for normal ops)&lt;/li&gt;
&lt;li&gt;And a &lt;strong&gt;direct connection&lt;/strong&gt; (for migrations)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But Prisma didn’t play well with pooled URLs. It caused weird, unexpected issues.&lt;/p&gt;
&lt;h4 id=&quot;fix-&quot;&gt;Fix:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Supabase’s &quot;stable connection string&quot;&lt;/strong&gt; for local + migrations&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Prisma Accelerate&lt;/strong&gt; in production with the correct URL&lt;/li&gt;
&lt;li&gt;Add the Accelerate URL to your &lt;code&gt;.env.production&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After that, things worked. Kinda.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;You may not have learned any &lt;em&gt;technical&lt;/em&gt; tricks from this post. But that’s the point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Things will break.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Abstractions will leak.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You’ll be stuck. Frustrated. Confused.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;But you’re a builder. And builders show up.&lt;/p&gt;
&lt;p&gt;That’s what matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-takeaway&quot;&gt;❤️ Takeaway&lt;/h3&gt;
&lt;p&gt;Everyone can teach you code. I want to teach you a mindset.&lt;/p&gt;
&lt;p&gt;If you remember just two things from this post:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Break things.&lt;/strong&gt; That’s how you learn.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Draw wireframes.&lt;/strong&gt; Ugly ones. It gives you momentum.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s it. You’ll be 10x faster than me if you just start.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;follow-my-journey&quot;&gt;Follow My Journey&lt;/h3&gt;
&lt;p&gt;I write about my dev rants, lessons, and experiments—sometimes messy, sometimes insightful. If this resonated, follow me. I don’t just rant—I teach too.&lt;/p&gt;
&lt;p&gt;Next up: how I broke and optimized my backend.&lt;/p&gt;
&lt;p&gt;Till then,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Break. Fix. Weep. Shine.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;— Love and peace,&lt;/p&gt;
&lt;p&gt;Your fellow dev&lt;/p&gt;
&lt;h3 id=&quot;farewell-amp-little-sendoff-gifts-for-curious-minds&quot;&gt;Farewell &amp;amp; Little Sendoff Gifts for Curious Minds&lt;/h3&gt;
&lt;p&gt;Thanks for sticking around! If you're curious to poke around or steal some ideas (I won't tell), here are a few goodies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Live App:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://feedback.swaroop101.me/&quot;&gt;Create Next App&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/PRASHANTSWAROOP001/feedback-nextjs-app&quot;&gt;https://github.com/PRASHANTSWAROOP001/feedback-nextjs-app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wireframes (Chaos Included):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://raw.githubusercontent.com/PRASHANTSWAROOP001/contact-page/refs/heads/main/wireframe2.jpg&quot;&gt;https://raw.githubusercontent.com/PRASHANTSWAROOP001/contact-page/refs/heads/main/wireframe2.jpg&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/PRASHANTSWAROOP001/contact-page/blob/main/wireframe1.jpg?raw=true&quot;&gt;https://github.com/PRASHANTSWAROOP001/contact-page/blob/main/wireframe1.jpg?raw=true&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>React</category>
<guid isPermaLink="true">https://coderlegion.com/4131/how-i-survived-my-first-full-stack-app-with-next-js</guid>
<pubDate>Fri, 18 Jul 2025 12:38:28 +0000</pubDate>
</item>
</channel>
</rss>