Let’s say you’re trying to work on a clone of Twitter to learn how something like that is made with Next.js. Sooner or later, you’re gonna hit a wall: someone wants to change their avatar. It's vibe coding time!
In order to get off the ground quicker, you had your friends send you their avatar images over a Discord DM, you put them in your avatars folder in your project, and then asked the Tigris model context protocol (MCP) server to upload them all to the bucket:

Let’s be real, sometimes you really do just need to do this and this genuinely can help you get unstuck. But when you do, also make sure to ask for the command to run yourself next time. This way you can learn without getting doomed to training wheels.

This is a really great way to turn vibe coding sessions into lessons that you can take with you for the rest of your career. Just be a little careful, language models are really good at making very convincing looking text that is about as false as birds being real. If you are not careful, you can end up taking something as true when you really should not.
When the vibe is off
In order to have better user empathy for people that do vibe coding, I had Cursor kick off and go nuts implementing a lot of the boilerplate involved with making a simple Twitter clone. I left it mostly alone while I was working on something else. This worked out pretty well for me until I tried to generate an avatar upload page.
Everything worked until I tried to upload an avatar:
✔ Compiled /avatar/upload 200 in 978ms
✔ Compiled /api/avatar/upload in 218ms
✖ Error: A "use server" file can only export async functions, found string.
Read more: https://nextjs.org/docs/messages/invalid-use-server-value
at [project]/src/utils/storage.ts [app-route] (ecmascript) (.next/server/chunks/[root of the server]__decd23c8._.js:144:241)
at [project]/src/app/api/avatar/upload/route.ts [app-route] (ecmascript) (src/app/api/avatar/upload/route.ts:2:0)
at Object.<anonymous> (.next/server/app/api/avatar/upload/route.js:6:9)
So I took a look. utils/storage.ts
seemed like a good place to start. It's
where Cursor put all the Tigris interactions. This code should only ever run
on the server, so it got marked as
"use server";
.
When you make a file full of "use server";
actions, it can't have anything in
it but async exported functions, but it found a string exported. I looked in the
file and instantly noticed what was wrong:
export const bucket = process.env.TIGRIS_BUCKET || "tigris-example";
I’ve seen a few Next.js apps have “configuration variables” like this, this
pattern says “the bucket name is the contents of the environment variable
TIGRIS_BUCKET
, but if that doesn’t exist, then use tigris-example
. This lets
you have a different bucket in testing than you do in production. However, this
is a string, a string is not an async function. The solution is to change
this to be an async function:
export const bucket = async () => process.env.TIGRIS_BUCKET || "tigris-example";
When I asked the AI to come up with an option, it struggled for a bit and then ended up wanting to make the file upload a client component instead.

When I saw this, I had to do a double take. It left me thinking:

Next.js client components don’t have access to the environment variables your server does. This is because many platforms use the environment to store configuration, and leaking those to the client on every request would be catastrophically bad, that is how you end up with all your customer information leaked on the dark web.
However, sometimes there are “low risk” keys like heavily restricted API keys or
write-only API keys for things like browser telemetry. Those are okay to send to
the client.
Next.js lets you send these variables to the client,
but you have to go out of your way to do it by naming them NEXT_PUBLIC_NAME
instead of just NAME
:
SECRET_PLANS="save the world from itself" # private, on the server
NEXT_PUBLIC_SECRET_PLANS="world domination" # public, on the client, everyone knows
Just so we’re clear: DO NOT PUT YOUR TIGRIS CREDENTIALS IN ANY CODE EVER, use the secret management tools that are baked into your platform, they’re there for a reason.
Can you spare me an any
?
When I was later in the hacking process, I had CI/CD set up to a Kubernetes cluster I had laying around. I pushed one of the avatar uploading changes and noticed CI failed:
13.10 ./src/components/FileUpload.tsx
13.10 7:31 Error: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any
13.10
13.10 info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/app/api-reference/config/eslint#disabling-rules
Looking at the bit in question (on line 7, near character 31):
interface FileUploadProps {
onUploadComplete?: (result: any) => void;
onError?: (error: Error) => void;
bucket?: string;
keyPrefix?: string;
}
This is a pretty common pattern: a handler function that runs when something is
done. This callback is how you do lightweight message passing between components
(and to a limited extent, this is largely what code did way back in the day
before we had async
, I remember the days of callback pyramids). But, we don’t
know what type the response is. In fact, we likely don’t care what type it is.
This any
here says “I don’t care what type this is”.
However any
is kind of a landmine waiting to happen. It’s okay in small doses,
but in general it’s a bad hammer to reach for. The solution here is to disable
the lint rule:
interface FileUploadProps {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
onUploadComplete?: (result: any) => void;
onError?: (error: Error) => void;
bucket?: string;
keyPrefix?: string;
}
This makes things build and everything's okay. We can just put the fire over with the other fire, it's all good, right?
Let’s see what the AI thought. It's been right before, so surely it'll be fine this time, right?

This is not the fix we wanted because React v19 does exist. Additionally, downgrading the version of React in a framework like Next.js is…catastrophically bad.
Sure, vibe will help you get out the door and ship something, but be very careful when you go out and just blindly accept what the language model says without thinking carefully. Payday loans do get you more money, but if you’re not careful the technical debt you rack up could cost you more than your project would ever bring in.
Switching between vibing and debugging can be a bit of a struggle, but it's necessary to check that the code you're generating doesn't have common errors. Though you can't make big mistakes like deleting an entire bucket of objects with the Tigris MCP server, your editor can still generate code that leaks keys or doesn't handle the right types. I hope this example has helped you think about how you verify the code after its generated.
Ready to launch that new b2b enterprise AI-optimized SaaS?
Open your focus playlist, let the vibes flow, and make your dreams a reality! Tigris will help make it happen!