<?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: odeds</title>
    <description>The latest articles on DEV Community by odeds (@odeds).</description>
    <link>https://dev.to/odeds</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F480961%2Ff2073ce1-ac93-4db3-af98-a5522732f048.png</url>
      <title>DEV Community: odeds</title>
      <link>https://dev.to/odeds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/odeds"/>
    <language>en</language>
    <item>
      <title>How I Built FeedLog: Three Repos, One Product</title>
      <dc:creator>odeds</dc:creator>
      <pubDate>Wed, 06 May 2026 05:27:55 +0000</pubDate>
      <link>https://dev.to/odeds/how-i-built-feedlog-three-repos-one-product-1pba</link>
      <guid>https://dev.to/odeds/how-i-built-feedlog-three-repos-one-product-1pba</guid>
      <description>&lt;p&gt;FeedLog turns GitHub issues into publish-ready changelog entries without leaving your repo. You drop &lt;code&gt;@feedlog publish&lt;/code&gt; in an issue comment, an AI draft appears for review, you approve it, and it shows up in your public changelog. Simple concept — but shipping it involved a handful of deliberate architecture decisions I want to write down while they're fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three repos, one product
&lt;/h2&gt;

&lt;p&gt;The codebase lives across three repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;feedlog-api&lt;/code&gt;&lt;/strong&gt; (private) — the Node backend: webhooks, AI processing, the public API your customers call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;feedlog-app&lt;/code&gt;&lt;/strong&gt; (private) — the web dashboard: OAuth, settings, changelog management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/FeedlogAI/feedlog-toolkit" rel="noopener noreferrer"&gt;&lt;code&gt;feedlog-toolkit&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; (public, MIT) — the embeddable SDK that customers drop into their own site to render the changelog widget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Splitting into three was a deliberate choice. The toolkit is the only piece customers integrate directly, so it makes sense for it to be public, independently versioned (we use Changesets), and separately releasable without touching internal code. The API and app ship independently too — a frontend deploy doesn't force an API restart, and vice versa.&lt;/p&gt;

&lt;p&gt;The toolkit is a Stencil-based monorepo that outputs true web components plus auto-generated React and Vue wrappers. One component source, three framework targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API stack
&lt;/h2&gt;

&lt;p&gt;The API is Node with &lt;strong&gt;Fastify&lt;/strong&gt; as the HTTP framework. Fastify's plugin system and built-in schema validation are a good fit for a small team: we use &lt;code&gt;fastify-type-provider-zod&lt;/code&gt; so every route is typed end-to-end from the Zod schema to the handler — no separate OpenAPI spec to keep in sync.&lt;/p&gt;

&lt;p&gt;For the database: &lt;strong&gt;Drizzle ORM&lt;/strong&gt; on top of &lt;strong&gt;Neon Postgres&lt;/strong&gt;. Neon gives us a serverless Postgres database with branching, which is useful for previewing migrations. Drizzle keeps the schema as TypeScript and generates SQL migrations via Drizzle Kit. We run migrations as a separate &lt;code&gt;tsx scripts/migrate.ts&lt;/code&gt; step, not at startup.&lt;/p&gt;

&lt;p&gt;Beyond the request/response path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BullMQ + Redis&lt;/strong&gt; handles async work — GitHub webhook events get queued immediately and processed by a separate worker, so the webhook endpoint always returns fast. The AI draft generation also runs through the queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Croner&lt;/strong&gt; runs scheduled tasks in-process: a webhook recovery job that redelivers failed GitHub hook payloads every 15 minutes, plus Sentry cron monitor heartbeats for all three processes (API, events worker, external worker).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;opossum&lt;/code&gt;&lt;/strong&gt; wraps the Postgres pool as a circuit breaker so a DB hiccup degrades gracefully instead of cascading into timeouts across all requests.&lt;/li&gt;
&lt;li&gt;Per-API-key rate limiting is stored in Redis via &lt;code&gt;@fastify/rate-limit&lt;/code&gt;, so limits survive restarts and work across multiple instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The app stack
&lt;/h2&gt;

&lt;p&gt;The dashboard is &lt;strong&gt;TanStack Start&lt;/strong&gt; (React SSR) with TanStack Router and TanStack Query for data fetching. UI is Tailwind CSS v4 with Radix UI primitives following the shadcn pattern. It deploys to &lt;strong&gt;Cloudflare Workers&lt;/strong&gt; via Wrangler — edge-deployed SSR with no cold start tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  DB design decisions
&lt;/h2&gt;

&lt;p&gt;This is the part I spent the most time thinking through, and all three decisions have held up well.&lt;/p&gt;

&lt;h3&gt;
  
  
  UUIDv7 as the primary key
&lt;/h3&gt;

&lt;p&gt;Every table uses UUIDv7 as its primary key, generated by a Postgres extension (&lt;code&gt;uuidv7()&lt;/code&gt; as the column default). UUIDv7 is time-ordered and monotonically increasing, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New rows always insert at the end of the B-tree index — no page splits, no fragmentation.&lt;/li&gt;
&lt;li&gt;The UUID itself encodes the creation timestamp, so we don't need a separate &lt;code&gt;created_at&lt;/code&gt; column on every table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one real downside: the Neon console and Drizzle Studio just show the UUID as a UUID. They don't decode it into a human-readable timestamp. It's a small operational annoyance — when you're scanning rows manually you can't immediately see when a record was created. We handle this by having a &lt;code&gt;extractCreatedAtFromUuid7&lt;/code&gt; SQL helper we call when we need the timestamp in a query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefixed public IDs
&lt;/h3&gt;

&lt;p&gt;Internal primary keys are UUIDs and never leave the system. Every table that gets exposed through the API also has a &lt;code&gt;public_id&lt;/code&gt; column: a short, URL-safe string with a meaningful prefix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;usr_a3b7kx9m2p1z&lt;/span&gt;   &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;
&lt;span class="nx"&gt;ins_q8tnrfw4j6yd&lt;/span&gt;   &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;installation&lt;/span&gt;
&lt;span class="nx"&gt;rep_c2mh5vp0xk3a&lt;/span&gt;   &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt;
&lt;span class="nx"&gt;iss_e9rz1db7yt4n&lt;/span&gt;   &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;
&lt;span class="nx"&gt;pk_lw6gc8nu0fqj&lt;/span&gt;    &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IDs are &lt;code&gt;prefix_&lt;/code&gt; + 12 characters of base36 nanoid (&lt;code&gt;customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 12)&lt;/code&gt;). The prefix serves as an immediate type hint when you see an ID in a log, a support ticket, or a URL — you know instantly what kind of entity you're dealing with. Stripe popularized this pattern for good reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Soft deletes on every table
&lt;/h3&gt;

&lt;p&gt;All tables have a &lt;code&gt;deleted_at&lt;/code&gt; timestamp column, and every delete — no matter how trivial — goes through a soft delete. Even rows that could safely be nuked immediately get &lt;code&gt;deleted_at&lt;/code&gt; set instead of being removed.&lt;/p&gt;

&lt;p&gt;The pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accidental recovery.&lt;/strong&gt; When something goes wrong in production and a record gets deleted it shouldn't have, you can restore it with an &lt;code&gt;UPDATE&lt;/code&gt;. No backup restore, no data archaeology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail.&lt;/strong&gt; You can always see what existed and when it was removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo flows are free.&lt;/strong&gt; Upvotes are a good example: when a user un-upvotes something, we set &lt;code&gt;deleted_at&lt;/code&gt;. When they re-upvote, we set &lt;code&gt;deleted_at = null&lt;/code&gt;. The code for "toggle" is trivial — no insert/delete cycle, just a field flip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safer debugging.&lt;/strong&gt; In production you can query soft-deleted rows alongside live ones to understand what happened, without the risk of it being too late.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The obvious tradeoff is that tables accumulate soft-deleted rows over time. The plan for that: a per-table cleanup cron that runs periodically and hard-deletes rows where &lt;code&gt;deleted_at&lt;/code&gt; is older than a configurable threshold. We already have &lt;code&gt;croner&lt;/code&gt; running in-process and the infrastructure for scheduled work, so this is a straightforward addition — each table can configure its own retention window before a permanent delete runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Honestly, not much yet. The main thing I'd reconsider is whether &lt;code&gt;croner&lt;/code&gt; running in-process in the API server is the right home for cleanup jobs long-term, or whether they should live in a separate scheduled job process. In-process is simpler to start with, but it means every API instance races to run the same cron, which requires a distributed lock. For now the jobs are idempotent enough that duplicate runs are harmless, but it's something to revisit as the system grows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>changelog</category>
    </item>
    <item>
      <title>Hello, FeedLog</title>
      <dc:creator>odeds</dc:creator>
      <pubDate>Sun, 26 Apr 2026 07:45:50 +0000</pubDate>
      <link>https://dev.to/odeds/hello-feedlog-2j93</link>
      <guid>https://dev.to/odeds/hello-feedlog-2j93</guid>
      <description>&lt;p&gt;This is our hello world post: a short answer to &lt;em&gt;why FeedLog exists&lt;/em&gt; and &lt;em&gt;who gets the most from it&lt;/em&gt;. If our homepage line resonates (“from issue chaos to changelogs users love”), this is the story behind it. For a full walkthrough from a messy GitHub issue to a changelog entry, open &lt;a href="https://feedlog.dev/#see-the-transformation-in-action" rel="noopener noreferrer"&gt;See the transformation in action&lt;/a&gt; on our homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we built it
&lt;/h2&gt;

&lt;p&gt;Most teams don’t fail at shipping. They fail at &lt;strong&gt;closing the loop&lt;/strong&gt;: turning what actually merged into something &lt;strong&gt;customers and users can find&lt;/strong&gt;, without another evening of manual release notes or copying tickets into a second system.&lt;/p&gt;

&lt;p&gt;We kept seeing the same pattern: engineering lives in &lt;strong&gt;GitHub issues&lt;/strong&gt; and PRs, while “public updates” live in a &lt;strong&gt;changelog tool, doc, or CMS&lt;/strong&gt; someone has to babysit. That seam creates duplicate work, stale pages, and the same &lt;strong&gt;“what changed?”&lt;/strong&gt; questions in email and support.&lt;/p&gt;

&lt;p&gt;FeedLog is our attempt to &lt;strong&gt;keep intake, feedback, and publish where the work already happens&lt;/strong&gt; in the repo, so user-facing comms isn’t the task that always slips. Rough work stays in issues; when you’re ready, you ship the words with &lt;strong&gt;&lt;code&gt;@feedlog publish&lt;/code&gt;&lt;/strong&gt; (&lt;a href="https://feedlog.dev/#command-reference" rel="noopener noreferrer"&gt;Full command list&lt;/a&gt;). Nothing user-facing goes live until you approve it, so raw technical titles don’t have to hit end users by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it’s best for
&lt;/h2&gt;

&lt;p&gt;FeedLog is aimed at &lt;strong&gt;small product teams (about 2 to 15 people)&lt;/strong&gt; who ship on &lt;strong&gt;GitHub&lt;/strong&gt; and want &lt;strong&gt;feedback and a public changelog&lt;/strong&gt; without maintaining a separate marketer-first CMS or retyping work into Notion or Confluence.&lt;/p&gt;

&lt;p&gt;You’re a great fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Developers own the loop&lt;/strong&gt;: you’d rather publish from an issue than log into another dashboard to “compose” an update.&lt;/li&gt;
&lt;li&gt;You want &lt;strong&gt;less context switching&lt;/strong&gt;: plan and ship in GitHub, not in a second tool that goes out of sync with reality.&lt;/li&gt;
&lt;li&gt;You care about &lt;strong&gt;trust and clarity&lt;/strong&gt;: users get &lt;strong&gt;clear release notes in one place&lt;/strong&gt;, with fewer confused pings and a steadier sense that the product is moving.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re &lt;strong&gt;GitHub-native&lt;/strong&gt; today (OAuth, issues, workflow). If your team doesn’t live in GitHub, we’re probably not the right tool yet, and we’d rather say that upfront than waste your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Less context switching, more shipping
&lt;/h2&gt;

&lt;p&gt;Our homepage puts it plainly: the same “what changed?” questions and copy-paste into a second tool drain time you wanted for shipping. FeedLog is built around &lt;strong&gt;less context switching, more shipping&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No extra CMS to manage&lt;/strong&gt; for every release. Drafts tie back to the issue you already have open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay in GitHub&lt;/strong&gt; for the loop that matters: when the draft looks right, &lt;strong&gt;&lt;code&gt;@feedlog publish&lt;/code&gt;&lt;/strong&gt; (&lt;a href="https://feedlog.dev/#command-reference" rel="noopener noreferrer"&gt;Full command list&lt;/a&gt;), not another tab to hunt down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup in under five minutes&lt;/strong&gt; is the bar we hold ourselves to, because a tool you don’t adopt doesn’t help anyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the same wedge we care about in positioning: &lt;strong&gt;feedback and changelog in one product for small teams&lt;/strong&gt;, with GitHub as the source of truth, so you’re not fighting adoption from engineers who hate “one more app.”&lt;/p&gt;

&lt;h2&gt;
  
  
  User updates and feedback, not two separate chores
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;User updates&lt;/strong&gt; shouldn’t be a parallel track that only happens when someone remembers. When issues turn into &lt;strong&gt;draft posts&lt;/strong&gt; you review, the changelog becomes a natural extension of how you already ship, not a weekend cleanup task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback&lt;/strong&gt; that stays tied to &lt;strong&gt;issues&lt;/strong&gt; keeps signal close to where you fix things. Users get a &lt;strong&gt;real changelog&lt;/strong&gt; they can rely on; your team spends less energy re-explaining releases in scattered threads.&lt;/p&gt;

&lt;p&gt;We’re biased toward &lt;strong&gt;human-in-the-loop&lt;/strong&gt;: automation helps with drafting and tone, but &lt;strong&gt;you decide&lt;/strong&gt; when something is ready for users. That’s how we square “move fast” with “don’t embarrass us with a cryptic issue title on the marketing site.”&lt;/p&gt;

&lt;p&gt;If this sounds like your team, we’d love you to try FeedLog: &lt;strong&gt;free to start&lt;/strong&gt;, same story you’ll see on &lt;a href="https://feedlog.dev/" rel="noopener noreferrer"&gt;the homepage&lt;/a&gt;. When you’re ready, we’re here for the harsh feedback that makes the product better, and you can see how we use FeedLog ourselves in &lt;a href="https://feedlog.dev/#do-what-you-preach" rel="noopener noreferrer"&gt;Do what you preach&lt;/a&gt; on that page. For setup, pricing, and how drafts compare to public posts, open &lt;a href="https://feedlog.dev/#frequently-asked-questions" rel="noopener noreferrer"&gt;Frequently asked questions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>changelog</category>
      <category>automation</category>
    </item>
    <item>
      <title>Enhancing Vue Development with the Render Paint Flashing Plugin</title>
      <dc:creator>odeds</dc:creator>
      <pubDate>Thu, 22 Jun 2023 17:38:32 +0000</pubDate>
      <link>https://dev.to/odeds/enhancing-vue-development-with-the-render-paint-flashing-plugin-4m5p</link>
      <guid>https://dev.to/odeds/enhancing-vue-development-with-the-render-paint-flashing-plugin-4m5p</guid>
      <description>&lt;p&gt;&lt;em&gt;It’s important to note that the Render Paint Flashing Plugin is designed to work exclusively in Vue development mode.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As Vue developers, optimizing performance and minimizing unnecessary re-renders is crucial to building high-quality applications. In this article, we’ll explore the Render Paint Flashing Plugin, a powerful tool that enhances component development in Vue 3 applications. Inspired by the Chrome DevTools Rendering paint flashing panel, this plugin provides a visual indicator for re-rendered components, enabling developers to identify potential performance bottlenecks and optimize their Vue applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual Indicator: Identifying Areas for Optimization
&lt;/h2&gt;

&lt;p&gt;The key feature of the Render Paint Flashing Plugin is its visual indicator that highlights re-rendered components on the screen. This feature provides developers with an intuitive way to identify areas that require optimization in their Vue applications.&lt;/p&gt;

&lt;p&gt;By enabling the Render Paint Flashing Plugin, you can observe which components are being re-rendered during different interactions or state changes. The plugin applies a flashing effect to these components, making them easily distinguishable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--skyK_4pi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0zk6845d9r3jk7uq356u.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--skyK_4pi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0zk6845d9r3jk7uq356u.jpeg" alt="Example of plugin usage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get started with the Render Paint Flashing Plugin, visit the &lt;a href="https://github.com/odeds/vue3-render-paint-flashing"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vue3</category>
      <category>vue</category>
    </item>
  </channel>
</rss>
