<?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: Alina Mueller</title>
    <description>The latest articles on DEV Community by Alina Mueller (@alina_mueller_47afcef2724).</description>
    <link>https://dev.to/alina_mueller_47afcef2724</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%2F3827690%2Fa5aa191d-4d02-46e2-9508-cbde2791bb2f.png</url>
      <title>DEV Community: Alina Mueller</title>
      <link>https://dev.to/alina_mueller_47afcef2724</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alina_mueller_47afcef2724"/>
    <language>en</language>
    <item>
      <title>How I Built a FiveM Mod Marketplace with Next.js 16 and Stripe</title>
      <dc:creator>Alina Mueller</dc:creator>
      <pubDate>Sat, 11 Apr 2026 20:38:09 +0000</pubDate>
      <link>https://dev.to/alina_mueller_47afcef2724/how-i-built-a-fivem-mod-marketplace-with-nextjs-16-and-stripe-5b9p</link>
      <guid>https://dev.to/alina_mueller_47afcef2724/how-i-built-a-fivem-mod-marketplace-with-nextjs-16-and-stripe-5b9p</guid>
      <description>&lt;p&gt;Building a FiveM mod marketplace from scratch is not your typical Next.js project. When I started working on &lt;a href="https://vertexmods.com" rel="noopener noreferrer"&gt;VertexMods&lt;/a&gt;, I quickly realized that the usual e-commerce patterns don't fully translate to the gaming modding space. Here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Before we dive in, here's the full tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router, React Server Components, PPR)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL + Drizzle ORM&lt;/strong&gt; (75 tables, full-text search via tsvector)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clerk&lt;/strong&gt; for authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe + Coinbase Commerce&lt;/strong&gt; for payments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare R2&lt;/strong&gt; for mod file storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript strict mode&lt;/strong&gt; throughout&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why FiveM Mods Are Different
&lt;/h2&gt;

&lt;p&gt;FiveM is a multiplayer modification framework for GTA V. Server owners run custom Lua scripts (called "resources") that add gameplay features — economy systems, vehicles, jobs, weapons, UI menus. These scripts range from free community projects to premium $50+ packages.&lt;/p&gt;

&lt;p&gt;The marketplace has to handle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instant digital delivery&lt;/strong&gt; — buyers expect the download link immediately after payment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest checkout&lt;/strong&gt; — many server owners don't want to create accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two payment methods&lt;/strong&gt; — traditional Stripe + crypto via Coinbase (the gaming community loves crypto)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free mods with monetized links&lt;/strong&gt; — Linkvertise/LinkHub for free downloads&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Stripe + Instant Downloads
&lt;/h2&gt;

&lt;p&gt;The trickiest part was the download flow. We use Stripe Checkout sessions with webhook verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/api/webhooks/stripe/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook signature verification failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleSuccessfulPurchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For guest users, we generate encrypted download tokens with a 7-day expiry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Encrypted token for guest downloads — no account needed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;productIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Next.js 16 + PPR
&lt;/h2&gt;

&lt;p&gt;We're on Next.js 16 with Partial Pre-Rendering (PPR) enabled via &lt;code&gt;cacheComponents: true&lt;/code&gt;. This replaced all the old &lt;code&gt;unstable_cache&lt;/code&gt; patterns with &lt;code&gt;"use cache"&lt;/code&gt; directives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Product listing page — static shell + dynamic personalization&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductGrid&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hours&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPublishedProducts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Grid&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shop page now serves from cache in ~50ms while the cart/auth state hydrates client-side. That's a real win for Core Web Vitals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drizzle ORM + Full-Text Search
&lt;/h2&gt;

&lt;p&gt;PostgreSQL full-text search powers the mod discovery. We use a generated tsvector column with a GIN index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// schema/products.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;defaultRandom&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;searchVector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tsvector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_vector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;generatedAlwaysAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`to_tsvector('english', title || ' ' || description)`&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products_search_idx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchVector&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Querying is clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`search_vector @@ plainto_tsquery('english', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`ts_rank(search_vector, plainto_tsquery('english', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)) DESC`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cloudflare R2 for Mod Files
&lt;/h2&gt;

&lt;p&gt;R2 stores the actual ZIP files. Presigned URLs expire after 15 minutes to prevent link sharing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/r2.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDownloadUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;R2_BUCKET_NAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r2Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// 15 min&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The R2 bucket is private — no public access. Downloads always go through our API which validates the token first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Language with next-intl
&lt;/h2&gt;

&lt;p&gt;The marketplace supports 4 locales: English (no prefix), German (&lt;code&gt;/de&lt;/code&gt;), French (&lt;code&gt;/fr&lt;/code&gt;), and Brazilian Portuguese (&lt;code&gt;/pt-br&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;English URLs are canonical without prefix — &lt;code&gt;/shop&lt;/code&gt; not &lt;code&gt;/en/shop&lt;/code&gt;. This is a common source of SEO bugs; make sure your sitemap, hreflang tags, and middleware all agree on this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creator Marketplace
&lt;/h2&gt;

&lt;p&gt;Beyond just selling our own mods, we built a creator platform where independent FiveM developers can list their scripts. Creators earn 70% of revenue, we take 30%. Stripe Connect handles payouts.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Creator applies → admin reviews&lt;/li&gt;
&lt;li&gt;Creator submits product for review&lt;/li&gt;
&lt;li&gt;Admin approves → product goes live&lt;/li&gt;
&lt;li&gt;Buyer purchases → creator gets 70% via Stripe Connect payout&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Webhooks are hard in development.&lt;/strong&gt; We built a zero-config webhook setup that automatically starts the Stripe CLI listener with &lt;code&gt;pnpm dev&lt;/code&gt; and writes the secret to &lt;code&gt;.env.local&lt;/code&gt;. No more manual setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PPR with auth is tricky.&lt;/strong&gt; Static cache can't include user-specific data. The pattern that works: cache the product data, render auth state client-side with a Suspense boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guest checkout UX matters.&lt;/strong&gt; A huge percentage of server owners will abandon checkout if forced to create an account. Guest checkout with email-based download tokens was worth the extra complexity.&lt;/p&gt;

&lt;p&gt;The full marketplace is live at &lt;a href="https://vertexmods.com" rel="noopener noreferrer"&gt;vertexmods.com&lt;/a&gt; if you want to see it in action.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions? Drop them in the comments. I'm happy to go deeper on any part of the stack.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>gaming</category>
    </item>
    <item>
      <title>How to Choose Quality FiveM Scripts for Your Roleplay Server</title>
      <dc:creator>Alina Mueller</dc:creator>
      <pubDate>Mon, 16 Mar 2026 16:31:05 +0000</pubDate>
      <link>https://dev.to/alina_mueller_47afcef2724/how-to-choose-quality-fivem-scripts-for-your-roleplay-server-3pcc</link>
      <guid>https://dev.to/alina_mueller_47afcef2724/how-to-choose-quality-fivem-scripts-for-your-roleplay-server-3pcc</guid>
      <description>&lt;p&gt;You've seen it happen. Someone installs a new script — looks great in the preview video — and suddenly the server is lagging, players are getting kicked, and the console is spewing errors nobody knows how to read. The script gets blamed, gets removed, and an hour of admin time is gone.&lt;/p&gt;

&lt;p&gt;Picking FiveM scripts well is a real skill. Here's how to actually do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework Compatibility First
&lt;/h2&gt;

&lt;p&gt;Before anything else: know your framework and be strict about it.&lt;/p&gt;

&lt;p&gt;ESX, QBCore, and QBox are not interchangeable. Scripts written for one often won't work on another without modification, and "conversion" ports range from clean rewrites to barely functional hacks. Always confirm what framework a script targets before you download anything.&lt;/p&gt;

&lt;p&gt;A few things to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ESX scripts&lt;/strong&gt; should reference &lt;code&gt;ESX:getSharedObject&lt;/code&gt; or &lt;code&gt;exports['es_extended']:getSharedObject()&lt;/code&gt;. If you see &lt;code&gt;ESX = nil&lt;/code&gt; at the top, that's the old initialization pattern — it works but signals an older, less-maintained codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QBCore scripts&lt;/strong&gt; use &lt;code&gt;exports['qb-core']:GetCoreObject()&lt;/code&gt; or the newer &lt;code&gt;exports.qb-core:GetCoreObject()&lt;/code&gt;. Watch for scripts that claim QBCore support but are just ESX conversions with a thin wrapper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QBox&lt;/strong&gt; is a fork of QBCore with breaking changes in the item and inventory systems. Scripts that haven't been updated for QBox's item metadata changes will break your inventory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a script's README doesn't clearly state framework compatibility and tested version numbers, treat that as a red flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Code Quality Signals
&lt;/h2&gt;

&lt;p&gt;You don't need to be a Lua expert to spot problematic code. A few things to scan for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database calls in loops.&lt;/strong&gt; Look for &lt;code&gt;exports.oxmysql:execute&lt;/code&gt; or &lt;code&gt;MySQL.Async.execute&lt;/code&gt; inside &lt;code&gt;for&lt;/code&gt; loops or event handlers that fire frequently. Every loop iteration hitting the database is a performance disaster waiting to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;while true do&lt;/code&gt; loops without proper waits.&lt;/strong&gt; Every permanent loop needs a &lt;code&gt;Citizen.Wait()&lt;/code&gt; call. A loop with &lt;code&gt;Citizen.Wait(0)&lt;/code&gt; runs every frame for every player. This tanks performance. Loops with no wait at all will crash the thread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global variable pollution.&lt;/strong&gt; If the script defines dozens of globals instead of using local variables, that's sloppy code that can conflict with other resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No dependency checking.&lt;/strong&gt; Good scripts verify their dependencies exist at startup and print clear errors if something's missing. Scripts that just silently fail or throw cryptic errors when a dependency isn't loaded waste your time debugging.&lt;/p&gt;

&lt;p&gt;The cfx.re forum has decent discussions on what good Lua practices look like — searching for "resource optimization" there turns up useful threads. GitHub is also worth checking: a script with regular commits, a proper issue tracker being used, and a changelog is a much safer bet than one with a single commit from two years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Before Production
&lt;/h2&gt;

&lt;p&gt;Never install directly to a live server. This should be obvious, but a lot of server owners skip this step because setting up a test environment feels like work.&lt;/p&gt;

&lt;p&gt;A basic test setup doesn't need to be elaborate — a local FiveM server with the same framework version as your production server is enough. Install the script, connect with a test account, and put it through the full user flow: trigger every event, use every item, run every job. Watch the server console for errors the entire time.&lt;/p&gt;

&lt;p&gt;Specifically watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;attempt to index a nil value&lt;/code&gt; — usually means a framework function or export wasn't found&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^1SCRIPT ERROR&lt;/code&gt; — any script error in production is a problem, even if it "seems fine"&lt;/li&gt;
&lt;li&gt;Resource monitor (&lt;code&gt;resmon&lt;/code&gt; command) — check the resource's ms usage during active use. Anything consistently above 0.5ms per frame for a single resource is worth investigating.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to Find Scripts Worth Installing
&lt;/h2&gt;

&lt;p&gt;The quality spread is enormous. The free section of the cfx.re forum has everything from excellent community contributions to scripts that haven't been updated since 2019. You'll need to read through the thread, check the release date, see if the author responds to bug reports, and make your own judgment call.&lt;/p&gt;

&lt;p&gt;For paid scripts, &lt;a href="https://vertexmods.com/shop" rel="noopener noreferrer"&gt;VertexMods&lt;/a&gt; is worth a look — the listings include framework compatibility info and the scripts go through a review process before being sold. Tebex is the other major marketplace; same advice applies: check the reviews, check when it was last updated, check if the seller responds to support questions.&lt;/p&gt;

&lt;p&gt;If you want to build jobs without writing code from scratch, the &lt;a href="https://vertexmods.com/jobs-creator" rel="noopener noreferrer"&gt;VertexMods Jobs Creator&lt;/a&gt; tool generates ESX/QBCore/QBox job configs — useful if you're standing up new jobs quickly. For free mods, &lt;a href="https://vertexmods.com/free-mods" rel="noopener noreferrer"&gt;vertexmods.com/free-mods&lt;/a&gt; has a curated catalog alongside the usual cfx.re forum hunting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shortcuts That Bite You
&lt;/h2&gt;

&lt;p&gt;A few things server owners do that consistently cause problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installing scripts without reading the SQL file.&lt;/strong&gt; Some scripts add columns to core tables or create tables with the same name as something you already have. Always read the SQL before running it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting client-side validation.&lt;/strong&gt; Scripts that only validate actions on the client are exploitable. If a script has no server-side checks on critical actions (giving items, triggering payouts, accessing menus), players will find the exploits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping the changelog.&lt;/strong&gt; When you update a script, read what changed. Framework updates sometimes change function signatures or deprecate events. A script that worked fine six months ago might need config changes after an update.&lt;/p&gt;

&lt;p&gt;The servers that run smoothly long-term aren't the ones with the most scripts — they're the ones where every installed resource was actually evaluated before it went live.&lt;/p&gt;

</description>
      <category>fivem</category>
      <category>gaming</category>
      <category>gamedev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
