<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vishal Mathur</title>
    <description>The latest articles on DEV Community by Vishal Mathur (@vishal_mathur_472bfbc5b73).</description>
    <link>https://dev.to/vishal_mathur_472bfbc5b73</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4105751%2F65531a08-0ff0-488f-a937-dc720e88ca89.jpg</url>
      <title>DEV Community: Vishal Mathur</title>
      <link>https://dev.to/vishal_mathur_472bfbc5b73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vishal_mathur_472bfbc5b73"/>
    <language>en</language>
    <item>
      <title>I built a social scheduler that now posts to DEV too</title>
      <dc:creator>Vishal Mathur</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/poststage/i-built-a-social-scheduler-that-now-posts-to-dev-too-1ikh</link>
      <guid>https://dev.to/poststage/i-built-a-social-scheduler-that-now-posts-to-dev-too-1ikh</guid>
      <description>&lt;h2&gt;
  
  
  PostStage schedules to 16 platforms via their official APIs. DEV.to just became one of them.
&lt;/h2&gt;

&lt;p&gt;I've been building &lt;a href="https://trypoststage.com" rel="noopener noreferrer"&gt;PostStage&lt;/a&gt; — a scheduler that&lt;br&gt;
queues one post to 16 platforms and fires each through that platform's official&lt;br&gt;
API.&lt;/p&gt;

&lt;p&gt;This week I added &lt;strong&gt;DEV.to&lt;/strong&gt; as a publishing target, which is why I'm posting&lt;br&gt;
about it here: I can now write a draft, set a time, and have it land on DEV,&lt;br&gt;
Hashnode-style, alongside the same content going to LinkedIn and X.&lt;/p&gt;

&lt;p&gt;The current list: Instagram, Facebook Pages, LinkedIn, X, YouTube Shorts,&lt;br&gt;
Threads, Pinterest, Google Business Profile, Bluesky, Mastodon, TikTok, Tumblr,&lt;br&gt;
Telegram, Discord, WordPress, and DEV.&lt;/p&gt;

&lt;p&gt;A few things I made deliberate calls on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Official APIs only.&lt;/strong&gt; No browser automation, no unofficial endpoints. It
costs features — Instagram won't let anyone publish to a personal account, so
PostStage doesn't pretend to — but nothing breaks the week a platform ships a
DOM change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QStash for delivery, not polling.&lt;/strong&gt; Each post is handed to Upstash with a
&lt;code&gt;notBefore&lt;/code&gt; timestamp, so a scheduled post is a queued message rather than a
row some cron has to notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens encrypted at rest&lt;/strong&gt; with AES-256-GCM, because a scheduler is a pile
of other people's access tokens and should be treated as one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a real product with paid plans, and there's a free tier if you just want to&lt;br&gt;
see whether the queue does what you need.&lt;/p&gt;

&lt;p&gt;Happy to answer anything about the platform integrations — the API quirks across&lt;br&gt;
16 of them have been the genuinely hard part, and I'll probably write that up&lt;br&gt;
separately.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>saas</category>
      <category>api</category>
    </item>
    <item>
      <title>I built a social scheduler that now posts to DEV too</title>
      <dc:creator>Vishal Mathur</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:50:00 +0000</pubDate>
      <link>https://dev.to/vishal_mathur_472bfbc5b73/i-built-a-social-scheduler-that-now-posts-to-dev-too-1amh</link>
      <guid>https://dev.to/vishal_mathur_472bfbc5b73/i-built-a-social-scheduler-that-now-posts-to-dev-too-1amh</guid>
      <description>&lt;h2&gt;
  
  
  PostStage schedules to 16 platforms via their official APIs. DEV.to just became one of them.
&lt;/h2&gt;

&lt;p&gt;I've been building &lt;a href="https://trypoststage.com" rel="noopener noreferrer"&gt;PostStage&lt;/a&gt; — a scheduler that&lt;br&gt;
queues one post to 16 platforms and fires each through that platform's official&lt;br&gt;
API.&lt;/p&gt;

&lt;p&gt;This week I added &lt;strong&gt;DEV.to&lt;/strong&gt; as a publishing target, which is why I'm posting&lt;br&gt;
about it here: I can now write a draft, set a time, and have it land on DEV,&lt;br&gt;
Hashnode-style, alongside the same content going to LinkedIn and X.&lt;/p&gt;

&lt;p&gt;The current list: Instagram, Facebook Pages, LinkedIn, X, YouTube Shorts,&lt;br&gt;
Threads, Pinterest, Google Business Profile, Bluesky, Mastodon, TikTok, Tumblr,&lt;br&gt;
Telegram, Discord, WordPress, and DEV.&lt;/p&gt;

&lt;p&gt;A few things I made deliberate calls on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Official APIs only.&lt;/strong&gt; No browser automation, no unofficial endpoints. It
costs features — Instagram won't let anyone publish to a personal account, so
PostStage doesn't pretend to — but nothing breaks the week a platform ships a
DOM change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QStash for delivery, not polling.&lt;/strong&gt; Each post is handed to Upstash with a
&lt;code&gt;notBefore&lt;/code&gt; timestamp, so a scheduled post is a queued message rather than a
row some cron has to notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens encrypted at rest&lt;/strong&gt; with AES-256-GCM, because a scheduler is a pile
of other people's access tokens and should be treated as one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a real product with paid plans, and there's a free tier if you just want to&lt;br&gt;
see whether the queue does what you need.&lt;/p&gt;

&lt;p&gt;Happy to answer anything about the platform integrations — the API quirks across&lt;br&gt;
16 of them have been the genuinely hard part, and I'll probably write that up&lt;br&gt;
separately.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>saas</category>
      <category>api</category>
    </item>
    <item>
      <title>QStash JWTs Expire Before Delivery: A Far-Future Scheduling Postmortem</title>
      <dc:creator>Vishal Mathur</dc:creator>
      <pubDate>Wed, 02 Sep 2026 08:59:00 +0000</pubDate>
      <link>https://dev.to/vishal_mathur_472bfbc5b73/qstash-jwts-expire-before-delivery-a-far-future-scheduling-postmortem-4e5j</link>
      <guid>https://dev.to/vishal_mathur_472bfbc5b73/qstash-jwts-expire-before-delivery-a-far-future-scheduling-postmortem-4e5j</guid>
      <description>&lt;p&gt;We schedule social posts to the exact second with Upstash QStash. Turns out the delivery signature can go stale before the message ever fires — here's the 401-into-a-dead-letter-queue bug, the fix, and the free-tier gotcha we hit chasing it.&lt;/p&gt;




&lt;p&gt;title: "QStash JWTs Expire Before Delivery: A Far-Future Scheduling Postmortem"&lt;br&gt;
description: "We schedule social posts to the exact second with Upstash QStash. Turns out the delivery signature can go stale before the message ever fires — here's the 401-into-a-dead-letter-queue bug, the fix, and the free-tier gotcha we hit chasing it."&lt;br&gt;
tags: showdev, serverless, webdev, nextjs&lt;/p&gt;

&lt;h2&gt;
  
  
  published: false
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="https://trypoststage.com" rel="noopener noreferrer"&gt;PostStage&lt;/a&gt;, a social-media scheduler — connect Instagram, LinkedIn, YouTube, Pinterest, etc., write a post once, pick an exact publish time. The scheduling engine is Upstash QStash: enqueue a message with a &lt;code&gt;notBefore&lt;/code&gt; timestamp, QStash holds it and delivers a webhook at that exact second. No polling loop, no cron guessing — it just fires.&lt;/p&gt;

&lt;p&gt;That worked great until users started scheduling things more than a few days out, and a slice of those posts silently died.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Posts scheduled roughly 4–5 days ahead were coming back from QStash as an &lt;strong&gt;HTTP 401&lt;/strong&gt; on delivery, retrying three times, then landing in QStash's dead-letter queue. They'd eventually fire — hours late — only because a separate recovery sweep happened to re-enqueue them. Nobody was touching auth code. The webhook worked fine for anything scheduled a day or two out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause: the signature has its own clock, separate from the delivery time
&lt;/h2&gt;

&lt;p&gt;QStash signs the webhook request as a JWT &lt;strong&gt;at the moment you enqueue the message&lt;/strong&gt;, not at delivery time. We verify it with &lt;code&gt;jose.jwtVerify()&lt;/code&gt;, which checks &lt;code&gt;exp&lt;/code&gt;/&lt;code&gt;nbf&lt;/code&gt; with zero clock tolerance. That signature has its own expiry — and if a message sits in QStash's queue long enough before its scheduled delivery slot, the &lt;em&gt;signature&lt;/em&gt; goes stale before the &lt;em&gt;message&lt;/em&gt; ever gets sent. QStash still delivers it (that part works exactly as documented) — it just arrives carrying a JWT that's already expired by the time our endpoint checks it. &lt;code&gt;Receiver.verify()&lt;/code&gt; rejects it, we return 401, QStash retries into the same expired signature three more times, then gives up and parks it in the DLQ.&lt;/p&gt;

&lt;p&gt;At creation time:&lt;br&gt;
scheduledAt within QSTASH_WINDOW_MS of now?&lt;br&gt;
YES → enqueue to QStash immediately (status → QUEUED)&lt;br&gt;
NO  → leave status SCHEDULED, qstashMessageId: null&lt;br&gt;
(expected state — not a failure, just "not due for dispatch yet")&lt;/p&gt;

&lt;p&gt;dispatchDuePosts() (runs on a daily cron, or a frequent external driver):&lt;br&gt;
find posts: SCHEDULED + no qstashMessageId + scheduledAt &amp;lt;= now + QSTASH_WINDOW_MS&lt;br&gt;
→ enqueue each to QStash now&lt;/p&gt;

&lt;p&gt;QStash still delivers at the exact scheduled second — the window only controls &lt;em&gt;how long a signature has to survive&lt;/em&gt; before it's used, never delivery accuracy. Default &lt;code&gt;QSTASH_WINDOW_MS&lt;/code&gt; is 24h, which piggybacks on a cron we already run daily and keeps every hold well under the ~4 day threshold where we saw 401s. A tighter setup points an external scheduler at a dedicated dispatch endpoint every ~5 minutes and shrinks the window to ~30 minutes, so signatures barely age before use — but that requires the frequent driver to actually be running, or early-scheduled posts wait for the daily sweep and fire late.&lt;/p&gt;

&lt;p&gt;We also added a recovery pass for anything that &lt;em&gt;did&lt;/em&gt; slip through: a QUEUED post, well past due, with zero publish-log entries (our webhook's first action is always logging "publishing started," so zero rows means the delivery never actually executed) gets its stale message cancelled and re-enqueued fresh. And the publish webhook does an atomic status-claim (&lt;code&gt;SCHEDULED|QUEUED → PUBLISHING&lt;/code&gt;) so an at-least-once redelivery or a recovery re-enqueue can never double-publish the same post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha hiding behind the "just poll more often" instinct
&lt;/h2&gt;

&lt;p&gt;The obvious way to shrink the signature-staleness window further is to run the dispatch driver more frequently. We tried that on our own free-tier Postgres (Neon) — and burned through a full month's compute allowance in days. Neon's free tier autosuspends the database after ~5 minutes idle; a driver polling faster than that never lets it sleep, so the database stays "active" around the clock even when there's nothing to do. It's an easy trap for anyone bootstrapping on free-tier infra: the fix for one problem (signature staleness) can quietly reintroduce a cost problem you already solved with autosuspend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If a queue signs its delivery payload at &lt;em&gt;enqueue&lt;/em&gt; time rather than &lt;em&gt;delivery&lt;/em&gt; time, a long hold can expire the signature before the message ever fires — check whether your queue's docs mention this before you assume "scheduled but not yet due" is a safe state to sit in indefinitely.&lt;/li&gt;
&lt;li&gt;A 401 on webhook delivery isn't always an auth &lt;em&gt;code&lt;/em&gt; bug — it can be a timing bug wearing an auth error's clothes.&lt;/li&gt;
&lt;li&gt;When you fix a timing problem by polling more often, check what else is on the clock you're now keeping alive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PostStage's scheduler runs on this today — posts fire to the exact second whether they're scheduled for five minutes from now or three months out. If you're building something similar on QStash (or any provider that signs webhooks at enqueue time), happy to compare notes in the comments.&lt;/p&gt;

&lt;p&gt;The exact-second delivery mechanism was never broken. The signature just wasn't built to survive being enqueued days in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: don't hand a message to QStash before you have to
&lt;/h2&gt;

&lt;p&gt;The real fix isn't in QStash's configuration — it's in not enqueueing far-future posts early at all. We hold them locally and only push to QStash once they're close to due:&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>This is my first post.</title>
      <dc:creator>Vishal Mathur</dc:creator>
      <pubDate>Wed, 02 Sep 2026 08:25:23 +0000</pubDate>
      <link>https://dev.to/vishal_mathur_472bfbc5b73/this-is-my-first-post-504o</link>
      <guid>https://dev.to/vishal_mathur_472bfbc5b73/this-is-my-first-post-504o</guid>
      <description>&lt;p&gt;Hi community, this is my first post hence, i am new here. &lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
