<?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: Lior Karaev</title>
    <description>The latest articles on DEV Community by Lior Karaev (@l_build).</description>
    <link>https://dev.to/l_build</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%2F4015457%2Fa06f94b3-fc2b-462d-9b46-8621e7552ae8.png</url>
      <title>DEV Community: Lior Karaev</title>
      <link>https://dev.to/l_build</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/l_build"/>
    <language>en</language>
    <item>
      <title>The Bug That Turned My Own Inbox Into a Phishing Target</title>
      <dc:creator>Lior Karaev</dc:creator>
      <pubDate>Thu, 16 Jul 2026 19:40:56 +0000</pubDate>
      <link>https://dev.to/l_build/the-bug-that-turned-my-own-inbox-into-a-phishing-target-49co</link>
      <guid>https://dev.to/l_build/the-bug-that-turned-my-own-inbox-into-a-phishing-target-49co</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;RamenHire (ramenhire.com) is a job board for bootstrapped, profitable startups — built with Next.js and Supabase. Job seekers apply, and companies submit listings through public forms with no login required. Every submission triggers an internal notification email, built from user-supplied fields, sent straight to the team's own inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lib/email-templates.ts&lt;/code&gt; interpolated user-controlled fields — applicant name, company name, "why interested," job description, and more — directly into raw HTML email strings with zero output encoding, including values embedded inside &lt;code&gt;href="..."&lt;/code&gt; attributes.&lt;/p&gt;

&lt;p&gt;That meant any of the site's four public forms (apply, post-a-job, subscribe, company registration) could inject arbitrary HTML into the notification email the team reads internally — no authentication required, no rate limit standing in the way of a single crafted submission. A malicious applicant name could silently smuggle in a fake "click here to verify your login" link, styled to look like it belonged in the template, sitting right next to the real "Review in Supabase →" button. This is a stored HTML-injection vector aimed squarely at whoever opens the inbox — a real phishing risk against the team itself, not a hypothetical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Commit &lt;a href="https://github.com/Lior-Karayev/RamenHire/commit/3aed95d21d835e05d3712be9cf12a5c13e386c6f" rel="noopener noreferrer"&gt;&lt;code&gt;3aed95d&lt;/code&gt;&lt;/a&gt; — specifically the &lt;code&gt;lib/email-templates.ts&lt;/code&gt; hunk. Added a small &lt;code&gt;escapeHtml()&lt;/code&gt; helper (standard &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;"&lt;/code&gt;, &lt;code&gt;'&lt;/code&gt; entity escaping) and applied it to every user-supplied value across all six email template functions — both plain text content and values embedded inside &lt;code&gt;href&lt;/code&gt; attributes, escaped &lt;em&gt;before&lt;/em&gt; the attribute string is built, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The fix required more than "wrap everything in a helper" — text-node context and attribute context aren't the same problem. A value escaped only for display text still allows a quote-breakout attack inside an &lt;code&gt;href="..."&lt;/code&gt; (e.g. &lt;code&gt;" onmouseover="...&lt;/code&gt;), so escaping has to happen before the attribute is constructed, not at final render.&lt;/p&gt;

&lt;p&gt;Verified this at two levels. First, locally: ran the actual template functions against a real injection payload —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"&amp;gt;&amp;lt;img src=x onerror=alert(1)&amp;gt;&amp;lt;script&amp;gt;alert(2)&amp;lt;/script&amp;gt;&amp;lt;a href="https://evil.example/phish"&amp;gt;click&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— plus a separate quote-based attribute-breakout string, confirming none of it survived unescaped.&lt;/p&gt;

&lt;p&gt;Then in production, end-to-end: submitted that same payload through the live site as a real application — real Cloudflare Turnstile challenge (not scripted around), real database insert, real notification email sent to the team inbox through the exact code path this fix touches. I opened the resulting email myself and confirmed the actual outcome: the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag never executed, the fake &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; never attempted to load, and the injected link rendered as inert, garbled text sitting inside obviously-escaped characters — not a working "click here" button.&lt;/p&gt;

&lt;p&gt;One honest caveat worth including rather than glossing over: in the screenshot below, the raw &lt;code&gt;evil.example&lt;/code&gt; URL still appears blue and clickable. That's Gmail's own auto-linkification of any bare URL it finds in plain text — completely independent of the fix, and something Gmail would do to &lt;em&gt;any&lt;/em&gt; plain-text email mentioning a URL, malicious or not. The actual thing this fix prevents — a disguised, legitimate-looking clickable button — is gone; the substring being technically linkable inside garbled text is a cosmetic side effect of Gmail's own behavior, not a gap in the fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8yb7j4t6min6fuxpsg03.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8yb7j4t6min6fuxpsg03.PNG" alt="Gmail inbox showing a received test email with subject line and body containing an HTML injection payload rendered as plain, garbled text with escaped characters instead of executing as markup — no script alert fired, no fake image loaded, and the injected link appears as inert text rather than a clickable button" width="799" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built and directed with Claude Code under my own review and testing — including designing the verification payloads and personally confirming the production result described above.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>RamenHire: Outreach, a Real Answer, and What I Still Don't Know</title>
      <dc:creator>Lior Karaev</dc:creator>
      <pubDate>Wed, 15 Jul 2026 06:28:21 +0000</pubDate>
      <link>https://dev.to/l_build/ramenhire-outreach-a-real-answer-and-what-i-still-dont-know-5cc3</link>
      <guid>https://dev.to/l_build/ramenhire-outreach-a-real-answer-and-what-i-still-dont-know-5cc3</guid>
      <description>&lt;p&gt;Two things happened this week that matter more than anything I built: I asked 13 real companies for permission, and a stranger's public feedback closed out a security thread that started three comments ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security thread closes out
&lt;/h2&gt;

&lt;p&gt;A reader (credited in the comments of my last post) found three real issues in RamenHire's Supabase setup over the course of a few days — an overly broad delete permission, a question about whether upload URLs were properly tied to real submissions, and, in the most recent round, a second broad delete grant I hadn't caught either. Each one got fixed and verified against the actual production environment, not just tested locally and assumed to carry over.&lt;/p&gt;

&lt;p&gt;The full audit — including a proper security-audit-with-severity-ratings pass, an HTML-injection fix in outbound emails, and one deliberately deferred item I'm tracking rather than rushing — is public in the repo now. Prompted entirely by someone reading closely and asking good questions in a comment section. That's a better security process than I would have run on my own, for free, because I wrote honestly about what broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outreach: 13 emails, 1 real response
&lt;/h2&gt;

&lt;p&gt;This week's actual work: reaching out to all 13 companies whose open roles I'd curated onto RamenHire before they knew the site existed. Permission-first, not "you're already listed, come claim it" — I've written before about why I killed that model.&lt;/p&gt;

&lt;p&gt;The honest tally so far: one response. Featurebase's founder (genuinely lovely exchange, handled by their own AI support tool, no less) asked me to take their listing down since they manage hiring through their own Notion board — and thanked me for asking first instead of just listing them. That's a real, if small, validation of the honest approach: even a "no" landed well, because it was actually their choice.&lt;/p&gt;

&lt;p&gt;Twelve are still silent, which is the expected outcome of cold outreach in week one, not a bad sign yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's coming next
&lt;/h2&gt;

&lt;p&gt;Company self-service is next on the build list — specifically, a way for a company to edit or remove their own listing without emailing me directly, using the same verified-email pattern already built for registration. Directly prompted by the Featurebase exchange: watching a real company ask "please take this down" made it obvious this needs to be self-serve, not something that routes through my inbox every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually don't know yet
&lt;/h2&gt;

&lt;p&gt;Two honest, open questions, and I'd genuinely like real answers rather than assumptions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For anyone who's looked at ramenhire.com cold&lt;/strong&gt; — does the homepage explain what this is and why "verified bootstrapped" matters, fast enough? Or does it need to work harder before someone decides whether to keep reading?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For founders specifically&lt;/strong&gt; — before you'd register a company profile, what would you want to see first? Proof of real traffic, other companies already listed, something else entirely? I don't have enough registered companies yet to learn this from usage, so I'm asking directly instead.&lt;/p&gt;

&lt;p&gt;Happy to hear blunt answers on either.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>discuss</category>
      <category>showdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>RamenHire, Week 1: What Happens When Strangers Actually Read Your Code</title>
      <dc:creator>Lior Karaev</dc:creator>
      <pubDate>Wed, 08 Jul 2026 07:35:32 +0000</pubDate>
      <link>https://dev.to/l_build/ramenhire-week-1-what-happens-when-strangers-actually-read-your-code-1ame</link>
      <guid>https://dev.to/l_build/ramenhire-week-1-what-happens-when-strangers-actually-read-your-code-1ame</guid>
      <description>&lt;p&gt;A week ago, I launched RamenHire — a job board only for bootstrapped, profitable startups. This week was less about building new features and more about the internet doing what the internet does best: finding the gaps I didn't know I'd left.&lt;/p&gt;

&lt;p&gt;Here's what actually happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification question that wouldn't go away
&lt;/h2&gt;

&lt;p&gt;RamenHire's whole pitch rests on one promise: every listed company is genuinely bootstrapped, not VC-funded. So when someone commented on Product Hunt asking, essentially, "okay, but &lt;em&gt;how&lt;/em&gt; do you actually verify that?" — it wasn't a gotcha, it was the single most important question anyone could ask.&lt;/p&gt;

&lt;p&gt;The honest answer at the time was: manually, by me, checking Crunchbase and a Google search. Not glamorous, but true. Saying that out loud publicly forced me to actually think about whether that's good enough, and to start being upfront about it rather than letting "verified ✓" do more rhetorical work than it's earned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Killing my own feature before it shipped to real users
&lt;/h2&gt;

&lt;p&gt;The original plan for company profiles was: pre-populate profiles for the 13 companies whose jobs I'd already curated, then let them "claim" their profile via a magic link when I reached out.&lt;/p&gt;

&lt;p&gt;Building it out, the framing started to bother me. These companies don't know RamenHire exists yet. Pre-building a profile for them — even a good-faith one — quietly implies a relationship that isn't there. So I scrapped the claim flow entirely and rebuilt it as pure self-registration: companies find RamenHire, register themselves, and I approve manually. Nothing gets built &lt;em&gt;for&lt;/em&gt; anyone without them asking first.&lt;/p&gt;

&lt;p&gt;It's slower. There's no clever growth hack of "look, you're already listed!" It also means the directory launches empty and stays empty until real companies show up on their own. But it's the version I can defend if anyone asks how it works — which, per the above, someone always eventually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing legal pages nobody will read, correctly, on the off chance someone does
&lt;/h2&gt;

&lt;p&gt;Privacy Policy and Terms of Use are the least exciting part of any indie project, and the easiest to fake with a template. I didn't want to do that, partly because RamenHire has real EU visitors (GDPR isn't hypothetical), and partly because I'd rather the page be accurate than impressive.&lt;/p&gt;

&lt;p&gt;So instead of filling in a template, I had the actual codebase audited first: what data gets collected, where it's stored, which third parties touch it, what cookies actually fire. Two findings from that audit mattered more than the policy text itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Analytics was firing unconditionally, for every visitor, with zero consent gate. Not a documentation gap — an actual compliance gap.&lt;/li&gt;
&lt;li&gt;Admin notifications were quietly landing in a personal Gmail inbox instead of the project's real address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both got fixed as part of writing the policy, not just described in it. The cookie banner now genuinely blocks GA4 until you accept — verified by checking that no cookie appears at all pre-consent, not just that a banner shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comment that actually mattered
&lt;/h2&gt;

&lt;p&gt;I wrote a separate post a few days back about a gnarly Supabase Storage RLS bug. A reader, Pon, left a comment pointing out that one of the example policies I'd shared — &lt;code&gt;authenticated&lt;/code&gt; role, read access to an entire bucket — would let any signed-in user read &lt;em&gt;any&lt;/em&gt; uploaded CV, not just their own. On RamenHire specifically, that policy is effectively admin-only today since there's no job-seeker account system, so nothing was actively exposed. But the policy itself didn't actually express "admin"; it expressed "anyone signed in," which is a landmine if that ever changes without someone remembering why.&lt;/p&gt;

&lt;p&gt;Then, a day later, a sharper follow-up from the same person: even the &lt;em&gt;write&lt;/em&gt; side has a gap — anonymous uploads to that bucket aren't tied to any real application, so nothing stops junk files from accumulating in storage that never came from an actual applicant. That's not a hypothetical either — it's the same category of risk as the rate-limiting and bot-protection work I'd just finished, just a door I'd left open on the Storage side instead of the API side.&lt;/p&gt;

&lt;p&gt;That's now fixed — direct anonymous uploads have been replaced with short-lived, signed upload URLs tied to a specific application or company record, minted server-side. There's nothing left to upload &lt;em&gt;to&lt;/em&gt; without a real submission behind it, and I verified this directly against production: a raw upload attempt using the public key now gets rejected outright.&lt;/p&gt;

&lt;p&gt;Genuinely one of the more useful comment threads I've had on anything I've written. Public work gets public scrutiny, and this week that scrutiny made the product better twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things actually stand
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Company registration is live (self-serve, admin-approved, email-verified before it even enters my review queue).&lt;/li&gt;
&lt;li&gt;Privacy Policy and Terms of Use are up, grounded in a real data audit, not filler.&lt;/li&gt;
&lt;li&gt;Rate limiting, real bot verification, and (as of this week) signed upload URLs are in place across every public form.&lt;/li&gt;
&lt;li&gt;Real registered companies: 0. Real job seeker applications: 0. That's the honest number, and it's exactly why next week is about outreach, not more features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm intentionally not chasing job seeker growth yet. A job board with no real employers isn't a job board; it's a list. So the next push is entirely about getting real, bootstrapped companies to actually register — cold outreach to the companies already featured, and direct conversations with hiring managers, before any push to bring in job seekers.&lt;/p&gt;

&lt;p&gt;If you run a bootstrapped, profitable company and you're hiring, I'd genuinely love to hear from you — &lt;a href="https://www.ramenhire.com/companies/register?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=week1-update" rel="noopener noreferrer"&gt;register a free company profile here&lt;/a&gt; and I'll take it from there. And if you spot something broken while looking, even better. That's basically been the whole engine of week 1.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>supabase</category>
      <category>security</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>How I Fixed Supabase Storage RLS Errors That the Dashboard UI Couldn't Solve</title>
      <dc:creator>Lior Karaev</dc:creator>
      <pubDate>Mon, 06 Jul 2026 07:18:59 +0000</pubDate>
      <link>https://dev.to/l_build/how-i-fixed-supabase-storage-rls-errors-that-the-dashboard-ui-couldnt-solve-3c</link>
      <guid>https://dev.to/l_build/how-i-fixed-supabase-storage-rls-errors-that-the-dashboard-ui-couldnt-solve-3c</guid>
      <description>&lt;p&gt;If you've ever spent hours staring at a 400 error on &lt;br&gt;
a Supabase Storage upload despite having RLS policies &lt;br&gt;
correctly configured in the dashboard — this is for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I'm building RamenHire 🍜 — a job board for bootstrapped &lt;br&gt;
startups. One of the features is CV upload: job seekers &lt;br&gt;
drag and drop their CV directly in an apply modal, it &lt;br&gt;
uploads to a private Supabase Storage bucket, and the &lt;br&gt;
file path gets saved to the applications table.&lt;/p&gt;

&lt;p&gt;Simple enough. Except it wasn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Error
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://[project].supabase.co/storage/v1/object/cvs/...
400 (Bad Request)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then after digging into the actual response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"403"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Invalid Compact JWS"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Tried First (That Didn't Work)
&lt;/h2&gt;

&lt;p&gt;I went straight to the Supabase dashboard and added &lt;br&gt;
RLS policies via the Storage UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow anonymous uploads (INSERT, anon role)&lt;/li&gt;
&lt;li&gt;Allow authenticated reads (SELECT, authenticated role)&lt;/li&gt;
&lt;li&gt;Allow authenticated deletes (DELETE, authenticated role)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looked correct. Saved. Tested. Still 400.&lt;/p&gt;

&lt;p&gt;I checked the bucket settings — private bucket, no file &lt;br&gt;
size limit, no MIME type restrictions. All fine.&lt;/p&gt;

&lt;p&gt;I verified the Supabase client was initialized correctly &lt;br&gt;
with the right environment variables. Fine.&lt;/p&gt;

&lt;p&gt;I checked the file path format, sanitized the filename, &lt;br&gt;
added contentType to the upload call. Still failing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Actual Problem
&lt;/h2&gt;

&lt;p&gt;Here's what took me too long to figure out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Supabase Storage dashboard UI creates policies on &lt;br&gt;
the bucket level — but RLS enforcement happens on &lt;br&gt;
&lt;code&gt;storage.objects&lt;/code&gt;, not on the bucket itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The UI policies weren't actually applying to the right &lt;br&gt;
place. They looked correct in the dashboard but weren't &lt;br&gt;
being enforced at the level Supabase actually checks &lt;br&gt;
during uploads.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Stop using the UI. Go to the SQL Editor and create &lt;br&gt;
the policies directly on &lt;code&gt;storage.objects&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Drop any existing UI-created policies first&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"Allow anonymous uploads"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"Allow authenticated reads"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"Allow authenticated deletes"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Allow anonymous users to upload to the cvs bucket&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Allow anonymous uploads"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cvs'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Allow authenticated users to read&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Allow authenticated reads"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cvs'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Allow authenticated users to delete&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Allow authenticated deletes"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cvs'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in the SQL Editor. Test your upload immediately.&lt;/p&gt;

&lt;p&gt;It worked for me on the first try after hours of &lt;br&gt;
debugging the wrong thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;Supabase Storage is built on top of Postgres. The actual &lt;br&gt;
files and their metadata are stored in the &lt;code&gt;storage.objects&lt;/code&gt; &lt;br&gt;
table in the &lt;code&gt;storage&lt;/code&gt; schema. When you upload a file, &lt;br&gt;
Supabase checks RLS policies on that table — not on &lt;br&gt;
the bucket configuration.&lt;/p&gt;

&lt;p&gt;The dashboard UI abstracts this away, but the abstraction &lt;br&gt;
is leaky. Policies created via the UI sometimes don't &lt;br&gt;
propagate correctly to &lt;code&gt;storage.objects&lt;/code&gt;, especially for &lt;br&gt;
the &lt;code&gt;anon&lt;/code&gt; role on INSERT operations.&lt;/p&gt;

&lt;p&gt;Going directly to SQL bypasses the abstraction entirely &lt;br&gt;
and creates the policies exactly where Supabase needs them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Key Difference
&lt;/h2&gt;

&lt;p&gt;UI-created storage policy:&lt;/p&gt;

&lt;p&gt;Targets: bucket configuration layer&lt;br&gt;
Result: sometimes works, sometimes doesn't&lt;/p&gt;

&lt;p&gt;SQL-created policy on storage.objects:&lt;/p&gt;

&lt;p&gt;Targets: the actual RLS table Supabase checks&lt;br&gt;
Result: always works&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus: How To Verify Your Policies Are Actually Applied
&lt;/h2&gt;

&lt;p&gt;Run this in the SQL Editor to confirm your policies &lt;br&gt;
exist on the right table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'storage'&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'objects'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see your policies here — they're real and will work.&lt;br&gt;
If you only see them in the Storage UI but not here — &lt;br&gt;
they're not actually enforced.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If Supabase Storage uploads return 400 or 403 errors &lt;br&gt;
despite correct-looking RLS policies in the dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open SQL Editor&lt;/li&gt;
&lt;li&gt;Drop the UI-created policies&lt;/li&gt;
&lt;li&gt;Recreate them directly on &lt;code&gt;storage.objects&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test immediately&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The UI is convenient but unreliable for storage RLS. &lt;br&gt;
SQL is the source of truth.&lt;/p&gt;




&lt;p&gt;Building RamenHire in public — a job board exclusively &lt;br&gt;
for bootstrapped, profitable startups. &lt;/p&gt;

&lt;p&gt;🍜 &lt;a href="https://ramenhire.com" rel="noopener noreferrer"&gt;ramenhire.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you found this useful or hit the same issue, &lt;br&gt;
drop a comment — would love to know how common &lt;br&gt;
this actually is.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I launched a job board for bootstrapped startups today — here's what the first day looked like</title>
      <dc:creator>Lior Karaev</dc:creator>
      <pubDate>Sat, 04 Jul 2026 19:40:44 +0000</pubDate>
      <link>https://dev.to/l_build/i-launched-a-job-board-for-bootstrapped-startups-today-heres-what-the-first-day-looked-like-4foe</link>
      <guid>https://dev.to/l_build/i-launched-a-job-board-for-bootstrapped-startups-today-heres-what-the-first-day-looked-like-4foe</guid>
      <description>&lt;h1&gt;
  
  
  I launched a job board for bootstrapped startups today — here's what the first day looked like
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Who I am
&lt;/h2&gt;

&lt;p&gt;I'm a full-stack developer who's been building side projects for a while, &lt;br&gt;
but always as a hobby. This week I decided to take it seriously — shipping &lt;br&gt;
real products, building in public, and documenting the journey honestly. &lt;br&gt;
RamenHire is my first serious attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;A friend got laid off from his third "high-growth" startup in four years. &lt;br&gt;
Great engineer. Terrible luck with employers chasing runway instead of revenue.&lt;/p&gt;

&lt;p&gt;That conversation stuck with me. Because there ARE great companies out there &lt;br&gt;
— Basecamp, Plausible, Transistor, Payhip — calm, profitable, founder-run &lt;br&gt;
businesses that have been growing steadily for years without ever taking a &lt;br&gt;
cent of VC money. Places where people actually stay for years.&lt;/p&gt;

&lt;p&gt;But there's nowhere to find jobs at them specifically. LinkedIn and Indeed &lt;br&gt;
are flooded with VC-backed startups. So I built RamenHire 🍜 — a job board &lt;br&gt;
exclusively for bootstrapped, self-funded startups.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually shipped today
&lt;/h2&gt;

&lt;p&gt;This wasn't just a landing page day. Here's what went live:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation → real product&lt;/strong&gt;: Started with a static landing page and Google &lt;br&gt;
Forms. By end of day, replaced everything with internal Supabase-backed forms, &lt;br&gt;
a CV upload system using Supabase Storage, and a proper Post a Job page at /post-job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CV upload with Supabase Storage&lt;/strong&gt;: Users can now drag and drop their CV &lt;br&gt;
directly in the apply modal. Files go to a private Supabase Storage bucket — &lt;br&gt;
no public URLs, admin-only access. Took longer than expected because of RLS &lt;br&gt;
policy gotchas (more on that below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin email notifications with Resend&lt;/strong&gt;: Every new job post request, &lt;br&gt;
application, and subscriber now triggers an instant email to me with all &lt;br&gt;
the details. No more manually checking Supabase tables. Resend was incredibly &lt;br&gt;
easy to set up — had it working in under an hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local Docker development environment&lt;/strong&gt;: Set up Supabase CLI with Docker &lt;br&gt;
for local development. Production DB stays untouched until changes are stable &lt;br&gt;
and explicitly pushed. Should have done this from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analytics and SEO&lt;/strong&gt;: Google Analytics 4, Google Search Console, sitemap, &lt;br&gt;
robots.txt, Open Graph tags, and JobPosting schema markup for rich results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Supabase RLS with Storage is not the same as RLS on tables.&lt;/strong&gt;&lt;br&gt;
I spent an embarrassing amount of time getting a 400 error on CV uploads. &lt;br&gt;
The policies were correctly set in the UI but uploads kept failing. Turns out &lt;br&gt;
Storage RLS policies need to target &lt;code&gt;storage.objects&lt;/code&gt; directly via SQL — &lt;br&gt;
the UI policies weren't applying correctly. Once I ran the SQL directly &lt;br&gt;
in the editor, it worked immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate the idea before writing a single line of real code.&lt;/strong&gt;&lt;br&gt;
I launched a static landing page first — no backend, no auth, just a headline &lt;br&gt;
and two Google Form links. Only after seeing real signups from 6 different &lt;br&gt;
countries did I invest in building the real product. This saved me weeks of &lt;br&gt;
building something nobody wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS propagation is always slower than you expect 😅&lt;/strong&gt;&lt;br&gt;
Spent way too long staring at "Pending" status on Resend's domain verification. &lt;br&gt;
It eventually resolved itself. Lesson: set it up and go do something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code is a genuine force multiplier.&lt;/strong&gt;&lt;br&gt;
100% of this was built with Claude Code. Not vibe coding — I reviewed every &lt;br&gt;
decision, understood every line, and caught several issues before they hit &lt;br&gt;
production. But the speed difference is real. What would have taken me a week &lt;br&gt;
of evenings took one focused day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Admin dashboard — approve/reject job posts without logging into Supabase&lt;/li&gt;
&lt;li&gt;Move hardcoded job listings to database-driven&lt;/li&gt;
&lt;li&gt;Stripe integration once validated&lt;/li&gt;
&lt;li&gt;Blog section for SEO content&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;33 users in the first few days from 6 countries (US, Germany, Israel, 
Canada, UK, South Korea)&lt;/li&gt;
&lt;li&gt;0 paying customers&lt;/li&gt;
&lt;li&gt;0 real job listings yet — still hardcoded&lt;/li&gt;
&lt;li&gt;Built in roughly one week of evenings and one full day&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  I'd love to hear from you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Would you use a job board specifically for bootstrapped companies?&lt;/li&gt;
&lt;li&gt;If you run a bootstrapped startup and are hiring — would you post there?&lt;/li&gt;
&lt;li&gt;What would make this more useful for you?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All job posts are free during early access. &lt;/p&gt;

&lt;p&gt;🍜 &lt;a href="https://www.ramenhire.com/" rel="noopener noreferrer"&gt;Visit RamenHire&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>startup</category>
      <category>showdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
