Issuing a JWT via SvelteKit API route throws `Cannot use `cookies.set(...)`...`
Cannot use
cookies.set(...) after the response has been generated
In my SvelteKit (1.29.1.
) I have an API endpoint that's used by the companion Chrome extension.
Extension grabs the cookie and sends it as a bearer in a fetch chain:
- first to /api/refresh
that's checking whether the token is still valid, resigns it if it's outdated, and returns it;
- then /api/upload
using the returned token attaches it again to the header, along with the body
.
However SvelteKit crashes right after the endpoint returns the new signed JWT. While the token is still valid (I set expiration to 60
while reproducing the bug) (see the screenshot).
My hooks.server.ts
implementation is based on the tutorials, minus the api route interception to catch the bearer.
I'm clueless. Any advice please? :)



45 Replies
try updating to the @supabase/ssr package https://supabase.com/docs/guides/auth/server-side/creating-a-client?framework=sveltekit
Creating a Supabase client for SSR | Supabase Docs
Configure Supabase client to use Cookies
It sounds as if this issue persists in the ssr as well, but I haven't tried switching myself https://github.com/supabase/auth-helpers/issues/466#issuecomment-1833564866.
GitHub
[SvelteKit] Error: Cannot use
cookies.set(...)
after the response...Bug report Describe the bug When using SvelteKit's new streaming promises feature, it seems to run into an issue setting cookies with the logic that runs in server hooks mentioned here. Here is...
So is the extension the one trying to set the cookie? I'm not sure I understand. The extension should not touch the cookie. It can instantiate the supabaseclient and just use the session jwt to query
No, sorry I probably wasn't clear.
Extension only grabs the cookie and then sends a request the SvelteKit's endpoint that's handling the supabase request.
The issue is that if user doesn't visit the actual web app to refresh the token and use the extension, server fails with
invalid token
something-something expired __ ago.
So I tried implementing the refresh mechanism on the SvelteKit. First the extension sends the token from the cookie to the /refresh endpoint that checks if it's still valid (to avoid the 401 essentially). Signs new JWT and sends it back. Then cookie uses that JWT to send another request to /upload endpoint with the new token in the header.
That's itit's hard to read your screen shots, but i believe you are trying to instantiate the supabase client 2 times. 1 for the top level hook and 1 for /api routes. This is not correct.
Hmm, I thought what I'm doing is I'm reinitiating with the user's JWT if it hits /api
your /api route should have a server.ts file for that logic
similar to how I do this to exchange a magic link code for a session
this is in my routes/api/auth/callback/server.ts
Right, but are you calling this route from inside the app or externally?
you can do either, it's an api end point
No I know, but if you're hitting it from the different origin, it doesn't have access to the bearer of the browser
+server.ts i mean. any +server files only run on the server and you can have them accessible via any rest call
so you need to attach an auth header
pass the auth header in your request and it would or pass it as a param
it's not like jwt are super sensitive
But how are you going to initiate the supabase client with it if you're already hitting the endpoint? :)
export const GET: Action = async ({ url, locals: { supabase } }) => {
here you're accessing supabase
from locals
but it relies on the session cookie
Your endpoint could read the auth header from the request
, sure, but at the time your supabase
is accessible in the locals
, it already has to have an authorization, otherwise supabase wouldn't be initialized and available in locals
.
Unless you can change the header of the supabase init inside the server endpoint, which I'm not sure is possible?
hooks.server.ts
that part is cut in 2 in my screenshots, so just to make sure we're on the same page
this essentially replaces the supabase
in the locals with the bearer in the headersIf you are setting your own authorization header just us supabase-js client. Don't involve auth-helpers at all for that particular call.
This sort of goes back to just minting your own jwt from the one you get so it does not expire though.
So basically just do https://supabase.com/docs/reference/javascript/initializing?example=with-additional-parameters in each of the api endpoints instead of using
supabase
from locals because they're created there by the createServerClient, which is the auth-helper
, sorta?
Sorry, @Socal, is this what you meant? I might have misunderstood youi didn't fully flesh this out. essentially your api endpoint to refresh your token would be something along these lines
Supabase JWT expiration issue
Troubleshoot and resolve JWT expiration in Supabase. Understand token lifecycle for secure access.
Ohhh, I see now... Let me try this!
you're just trying to get a new session back into your chrome extention. it doesn't make sense to have it tied up into hooks because you're not using that all over your app
I'm half-way there, but just to make sure, it looks like whatever uses the endpoint would also need to pass in the
refresh_token
, along with the access_token
, otherwise the refershSession()
wouldn't work, right? refreshSession()
tries to getSession()
if no refresh_token
is passed in, and it won't find any inside the endpoint.
Or another option is to return a freshly minted JWT
if jwt.verify()
returns TokenExpiredError
, which seems a bit nuclear to me, if there's a refersh method available?There is not a method to refresh without burning the refresh_token which then kills your other clients using it.
Oh right, you mentioned this...
So newly minted JWT is the only way then
I wish it said that here though https://supabase.com/docs/reference/javascript/auth-refreshsession?example=refresh-session-using-a-passed-in-session. It just says it's going to return one
maybe it's logical, I'm just too noob for it lolol
Yeah. The jwt auth section discusses how refresh tokens work with the jwt (this is not a Supabase specific thing). It is for security of the jwt so if someone gets hold of them you can kill the refresh token with a signout so they can't keep refreshing the jwt. If you could keep using the same refresh token over and over then you have no way to stop it.
Feel like you are going thru the wringer on this...
But if you go the mint jwt route this call is useful https://supabase.com/docs/reference/javascript/auth-admin-getuserbyid to get the current claims you want to mint based on having the user id.
Currently trying to understand how a newly minted JWT becomes instantly expired when used, even though I'm setting the new
expiresIn
:


Are you sure it is 1hr and not 1h. Not sure which library you are using.
Also, what are you doing in this code as far as hitting the database/storage etc? Unless you need to meet RLS you can also just user service_role key and query using the user_id directly on tables. Then you don't mess with the jwt at all.
jsonwebtoken
. Pff you're right, noticed it just now too.
The endpoint:
- uploads file(s) to storage into user's folder. That triggers some pgsql functions;
- generates publicUrls fo the files;
- then it looks for the newly created records (by the function) and updates some columns.
Unsure really if I could skip the RLS here?
they do need to have access to the storage/tablesYeah, I think that needs the jwt to fill in the owner column in storage and also if you do any triggers with auth.uid() or defaults.
So I managed to set everything up with the JWT issuance logic correctly and extension works fine.
But now I have a broader API/auth architecture question. What if the same API endpoints are used by both the web app and the external clients? Issuing a JWT for every external client request based on the cookie might be fine, but if user is inside the web app and
+page.server.ts
are making requests to +server.ts
endpoints, I'd want this to be handled with auth-helpers (supabase/ssr), wouldn't I?
Trying to figure this out 🤔yes, you typically want to use the auth helper library as it makes passing the supabase instance around your app much easier using locals.
Thank you! Do you think I should set up some sort of a origin check in the hooks? Say if the api request coming from the same origin, then initiate with local session, otherwise look for bearer?
i would think the sveltekit like way would be to segragate out at the root route level folders for (extention) (app)
where any end points under the (extention) folder handle the request logic one way and anything under the (app) folder handles it a different way
SvelteKit docs
Advanced routing • SvelteKit documentation
Hmm, I see. I sort of do that already, but for me it won't be just the (extension), it's a general api that's to be used by other clients as well. The query logic is the same, as to avoid duplication I thought general authorization might be a good idea
you're the dev, set it up however you want
Thanks for all the advice! You guys are awesome!
Deployed the entire mechanism and the extension works fine, but now I'm not meeting some RLS conditions on the web. For example private stuff that's supposed to be
(auth.uid() = user_id)
is not visible even for me as the user and owner of the stuff.
Idk if anything changed in supabase/ssr drastically (doesn't look like it), but I suspect it's a conflict of different supabase clients trying to reuse the same cookie for API (createClient
) and web pages (createServerClient
, createBrowserClient
).Supabase.js clients do not use cookies.
Only auth-helpers/ssr ones do.
100%. This is what I do briefly:
So in all cases hooks supplies
supabase
. It's just a different initiation. Then for example layout page (where I'm having RLS issues) does this:
And then profile endpoint just runs the supabase query.
But my layout.ts
which is a browser env is left as is here https://supabase.com/docs/guides/auth/server-side/creating-a-client?framework=sveltekit&environment=layout.So you should set persistSession:false in that first client, just in case. I don't think local storage can be involved as you are using a serverclient.
Even tried like this, no luck

I would always do that on any client you are not using user sessions for just as default.
The more I work on this, the more I feel like hosting API in the same SvelteKit is not the best idea. I'm planning on iOS and macOS apps, as well as Chrome extension (already in flight). So that's why I thought I needed API.
But after spending 3 days on just rewriting API authorization logic, I feel like initiating supabase inside each client could've been faster 🤷♂️
The biggest downside is writing repetitive queries with supabase clients which essentially are going to query the same thing.
Experience I guess!