{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"sessions-cookies-and-who-you-are","title":"Sessions, cookies, and who the server thinks you are","url":"https://addaly.com/learn/building-apps-with-ai/sessions-cookies-and-who-you-are","position":53,"of":87,"minutes":8,"answer":"Every request is a stranger until the server looks up the session cookie, so the user's identity must come from that lookup and never from anything the request body says about itself.","keyIdea":"Every request is a stranger until the server looks up the session cookie, so the user's identity must come from that lookup and never from anything the request body says about itself."},"body":{"format":"markdown","text":"## HTTP has no memory\n\nEvery request arrives on its own. The server that handled your sign-in a minute ago does not remember you; on a serverless host it may not even be the same process. So something in each request has to say who is making it, and the browser's mechanism for that is the cookie.\n\n## What a session cookie is\n\nAfter sign-in, the server generates a long random string, stores it in a `sessions` table beside your user id and an expiry, and tells the browser to keep it:\n\n```text\nSet-Cookie: session=k8Fz...3Qa; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000\n```\n\nFrom then on the browser attaches that cookie to every request to your domain, without any code on your side asking. The server reads it, looks up the row, and now knows who you are. Signing out deletes the row; the cookie in the browser becomes a string that matches nothing.\n\nThe three flags matter. `HttpOnly` means JavaScript in the page cannot read the cookie, so a script injected by an attacker cannot steal it. `Secure` means it travels only over HTTPS. `SameSite=Lax` means other sites cannot make your browser send it with their requests, which closes a whole class of attack in one word. The auth library sets all three; check that they are there in the browser's devtools, under Application, Cookies, because a model hand-rolling a session will forget at least one.\n\n## Where identity comes from\n\nThis is the sentence the rest of the course leans on. **The server learns who you are by looking up the cookie. Nothing else in the request is evidence of identity.**\n\nGenerated code breaks this in a specific, common way. A route accepts a JSON body containing `userId` and uses it, because the front-end code that calls the route helpfully includes it. It works in every test, because the front end sends the real id. It is also an invitation: anyone can open the developer tools, change the id, and read someone else's rows. The server had no way to know, because it never asked the one source it could trust.\n\nThe fix is a pattern, applied to every route:\n\n```typescript\nconst session = await getSession(request);      // reads the cookie, looks it up\nif (!session) return new Response(\"Unauthorized\", { status: 401 });\nconst rows = await db.select().from(bookings).where(eq(bookings.userId, session.userId));\n```\n\nThe id comes from the session, the body is used only for what the body is for, and a request with no valid cookie is turned away before it reaches the database.\n\n## Two ways to store a session\n\nThe database session above is one row per login. It can be revoked instantly (delete the row) and it costs one lookup per request.\n\nThe alternative is a signed token, often a JWT, where the server puts the user id and expiry into the cookie itself and signs it, so it can verify the cookie without a lookup. Faster, and harder to revoke: the token is valid until it expires, wherever it is, and a \"log out everywhere\" button has to be built with extra machinery. For a small app the database session is simpler and its cost is invisible. If a library gives you the token kind, keep the expiry short.\n\n## Expiry and the second device\n\nSessions should end. Thirty days for a consumer app, hours for anything that handles money, and the auth library has a setting. A user signed in on a phone and a laptop has two session rows, and signing out on one should not sign out the other unless they ask for that. Test both, because the version where signing out anywhere signs out everywhere is the one that gets written by accident.\n\n## Reading a session in a server component\n\nIn frameworks that render on the server, the same lookup happens before the page is built. A page that shows \"Welcome, Priya\" fetched the session on the server and rendered with it. If the same page works when signed out and shows \"Welcome, undefined\", the check was never there, only the greeting.\n\n## Your users table, not theirs\n\nWhichever library or provider authenticates, keep a `users` table of your own, and have every other table reference it. The library's user record can be swapped, merged or migrated; your `user_id` foreign keys stay put. This is the shape that lets an app move from one provider to another with one migration instead of a rewrite, and it is a decision that is nearly free on day one and expensive on day three hundred.\n\n> Figure: Where the route got the user id. A two by two grid, rows From the session cookie and From the request body, columns Own id sent and Another id typed in: From the session cookie and Own id sent: Your rows; From the session cookie and Another id typed in: Your rows; From the request body and Own id sent: Your rows; From the request body and Another id typed in: Their rows. Three of these four cells behave identically in every test, because your own front end always sends the real id. The fourth is what happens when somebody edits it in the developer tools."},"check":{"question":"A route reads userId from the JSON body of the request and returns that user's bookings. The developer tests it while logged in and it returns the right rows. What is wrong?","options":[{"text":"Nothing; the body is sent by the app's own front-end code, so in practice it always contains the id of the user who is currently logged in.","correct":false,"tempting":"The app's code sends it in the normal case; anyone with the developer tools can send any id they like, and the server has no way to tell the difference."},{"text":"The route will be slow because it does not use the session cache.","correct":false,"tempting":"Performance is not the issue; the issue is that the identity is asserted by the caller rather than established by the server."},{"text":"The identity should come from the session cookie lookup, because the body is whatever the caller chose to send.","correct":true},{"text":"The route should use a POST instead of a GET.","correct":false,"tempting":"The method changes nothing about who is trusted for the identity; a body can be sent with either."}]},"prev":{"slug":"sign-in-without-writing-it-yourself","title":"Sign-in, without writing it yourself","url":"https://addaly.com/learn/building-apps-with-ai/sign-in-without-writing-it-yourself"},"next":{"slug":"files-and-uploads","title":"Files and uploads, and where they must not go","url":"https://addaly.com/learn/building-apps-with-ai/files-and-uploads"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Sessions, cookies, and who the server thinks you are', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/sessions-cookies-and-who-you-are (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Sessions, cookies, and who the server thinks you are","courseTitle":"Building Apps with AI","module":{"index":6,"slug":"the-parts-that-persist","title":"The parts that persist: data, accounts and files"},"answer":null,"keyIdea":"Every request is a stranger until the server looks up the session cookie, so the user’s identity must come from that lookup and never from anything the request body says about itself.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"cols":["Own id sent","Another id typed in"],"rows":["From the session cookie","From the request body"],"type":"matrix","cells":[["Your rows","Your rows"],["Your rows","Their rows"]],"title":"Where the route got the user id","caption":"Three of these four cells behave identically in every test, because your own front end always sends the real id. The fourth is what happens when somebody edits it in the developer tools.","cellLabels":[["correct","the body is ignored"],["correct","the hole"]]},"alt":"Figure: Where the route got the user id. A two by two grid, rows From the session cookie and From the request body, columns Own id sent and Another id typed in: From the session cookie and Own id sent: Your rows; From the session cookie and Another id typed in: Your rows; From the request body and Own id sent: Your rows; From the request body and Another id typed in: Their rows. Three of these four cells behave identically in every test, because your own front end always sends the real id. The fourth is what happens when somebody edits it in the developer tools."}},"blocks":[{"t":"h2","id":"http-has-no-memory","runs":[{"text":"HTTP has no memory"}]},{"t":"p","runs":[{"text":"Every request arrives on its own. The server that handled your sign-in a minute ago does not remember you; on a serverless host it may not even be the same process. So something in each request has to say who is making it, and the browser’s mechanism for that is the cookie."}]},{"t":"h2","id":"what-a-session-cookie-is","runs":[{"text":"What a session cookie is"}]},{"t":"p","runs":[{"text":"After sign-in, the server generates a long random string, stores it in a "},{"text":"sessions","code":true},{"text":" table beside your user id and an expiry, and tells the browser to keep it:"}]},{"t":"code","lang":"text","label":"Text","text":"Set-Cookie: session=k8Fz...3Qa; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000"},{"t":"p","runs":[{"text":"From then on the browser attaches that cookie to every request to your domain, without any code on your side asking. The server reads it, looks up the row, and now knows who you are. Signing out deletes the row; the cookie in the browser becomes a string that matches nothing."}]},{"t":"p","runs":[{"text":"The three flags matter. "},{"text":"HttpOnly","code":true},{"text":" means JavaScript in the page cannot read the cookie, so a script injected by an attacker cannot steal it. "},{"text":"Secure","code":true},{"text":" means it travels only over HTTPS. "},{"text":"SameSite=Lax","code":true},{"text":" means other sites cannot make your browser send it with their requests, which closes a whole class of attack in one word. The auth library sets all three; check that they are there in the browser’s devtools, under Application, Cookies, because a model hand-rolling a session will forget at least one."}]},{"t":"h2","id":"where-identity-comes-from","runs":[{"text":"Where identity comes from"}]},{"t":"p","runs":[{"text":"This is the sentence the rest of the course leans on. "},{"text":"The server learns who you are by looking up the cookie. Nothing else in the request is evidence of identity.","b":true}]},{"t":"p","runs":[{"text":"Generated code breaks this in a specific, common way. A route accepts a JSON body containing "},{"text":"userId","code":true},{"text":" and uses it, because the front-end code that calls the route helpfully includes it. It works in every test, because the front end sends the real id. It is also an invitation: anyone can open the developer tools, change the id, and read someone else’s rows. The server had no way to know, because it never asked the one source it could trust."}]},{"t":"p","runs":[{"text":"The fix is a pattern, applied to every route:"}]},{"t":"code","lang":"typescript","label":"TypeScript","text":"const session = await getSession(request);      // reads the cookie, looks it up\nif (!session) return new Response(\"Unauthorized\", { status: 401 });\nconst rows = await db.select().from(bookings).where(eq(bookings.userId, session.userId));"},{"t":"p","runs":[{"text":"The id comes from the session, the body is used only for what the body is for, and a request with no valid cookie is turned away before it reaches the database."}]},{"t":"h2","id":"two-ways-to-store-a-session","runs":[{"text":"Two ways to store a session"}]},{"t":"p","runs":[{"text":"The database session above is one row per login. It can be revoked instantly (delete the row) and it costs one lookup per request."}]},{"t":"p","runs":[{"text":"The alternative is a signed token, often a JWT, where the server puts the user id and expiry into the cookie itself and signs it, so it can verify the cookie without a lookup. Faster, and harder to revoke: the token is valid until it expires, wherever it is, and a “log out everywhere” button has to be built with extra machinery. For a small app the database session is simpler and its cost is invisible. If a library gives you the token kind, keep the expiry short."}]},{"t":"h2","id":"expiry-and-the-second-device","runs":[{"text":"Expiry and the second device"}]},{"t":"p","runs":[{"text":"Sessions should end. Thirty days for a consumer app, hours for anything that handles money, and the auth library has a setting. A user signed in on a phone and a laptop has two session rows, and signing out on one should not sign out the other unless they ask for that. Test both, because the version where signing out anywhere signs out everywhere is the one that gets written by accident."}]},{"t":"h2","id":"reading-a-session-in-a-server-component","runs":[{"text":"Reading a session in a server component"}]},{"t":"p","runs":[{"text":"In frameworks that render on the server, the same lookup happens before the page is built. A page that shows “Welcome, Priya” fetched the session on the server and rendered with it. If the same page works when signed out and shows “Welcome, undefined”, the check was never there, only the greeting."}]},{"t":"h2","id":"your-users-table-not-theirs","runs":[{"text":"Your users table, not theirs"}]},{"t":"p","runs":[{"text":"Whichever library or provider authenticates, keep a "},{"text":"users","code":true},{"text":" table of your own, and have every other table reference it. The library’s user record can be swapped, merged or migrated; your "},{"text":"user_id","code":true},{"text":" foreign keys stay put. This is the shape that lets an app move from one provider to another with one migration instead of a rewrite, and it is a decision that is nearly free on day one and expensive on day three hundred."}]}],"check":{"question":[{"text":"A route reads userId from the JSON body of the request and returns that user’s bookings. The developer tests it while logged in and it returns the right rows. What is wrong?"}],"options":[{"text":[{"text":"Nothing; the body is sent by the app’s own front-end code, so in practice it always contains the id of the user who is currently logged in."}],"correct":false,"tempting":[{"text":"The app’s code sends it in the normal case; anyone with the developer tools can send any id they like, and the server has no way to tell the difference."}]},{"text":[{"text":"The route will be slow because it does not use the session cache."}],"correct":false,"tempting":[{"text":"Performance is not the issue; the issue is that the identity is asserted by the caller rather than established by the server."}]},{"text":[{"text":"The identity should come from the session cookie lookup, because the body is whatever the caller chose to send."}],"correct":true,"tempting":null},{"text":[{"text":"The route should use a POST instead of a GET."}],"correct":false,"tempting":[{"text":"The method changes nothing about who is trusted for the identity; a body can be sent with either."}]}],"order":[1,3,0,2],"why":null},"prev":{"slug":"sign-in-without-writing-it-yourself","title":"Sign-in, without writing it yourself","minutes":9},"next":{"slug":"files-and-uploads","title":"Files and uploads, and where they must not go","minutes":8}}}