# Sessions

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Server Development
Source: https://www.devtutorials.ai/learn/astro/server/sessions

> What sessions are, sessions vs cookies, session drivers and the adapter defaults, session.get() and set(), what you can store, regenerate() and destroy(), Astro.session and context.session, disabling sessions and typing App.SessionData.

## Sessions

- A cookie is stored in the browser. The visitor can see it and change it.
- A **session** stores data on the **server** instead. The browser only keeps a random **session ID** in a cookie.
- On each request, Astro reads the ID and finds that visitor's data.
- In this lesson, you will store per-visitor data safely with `Astro.session`.

**Simple lesson:** a session keeps a visitor's data on the server, linked by an ID cookie.

### A Real-World Example

- Asha adds a "Recently viewed posts" box.
- The list belongs to one reader, and may grow long.
- In a cookie, the whole list would travel with every request, and the reader could edit it.
- In a session, the list stays on the server. The browser only holds a short ID.

Press **▶ Run**. Open a few posts, then go back home. The **Terminal** shows the session being created.

<ul>
    <li><a href="/posts/why-astro">Why I chose Astro</a></li>
    <li><a href="/posts/sessions">Sessions explained</a></li>
    <li><a href="/posts/cookies">Cookies 101</a></li>
</ul>
<h2>Recently viewed</h2>
{recent.length ? <ol>{recent.map((slug) => <li>{slug}</li>)}</ol> : <p>Nothing yet.</p>}
`,
    "src/pages/posts/[slug].astro": `---
const { slug } = Astro.params;
const recent: string[] = (await Astro.session?.get("recent")) ?? [];
const updated = [slug, ...recent.filter((s) => s !== slug)].slice(0, 3);
Astro.session?.set("recent", updated);
---

<h1>Post: {slug}</h1>
<a href="/">Home</a>
`,
  }}
/>

What happened:

- The first `set()` created a session. Astro sent a cookie called `astro-session` with a random ID.
- Each post page added its slug to the list on the server.
- The home page read the list back with `get()`.

### What You Will Learn

In this lesson, you will learn:

1. What sessions are and how they differ from cookies.
2. How session drivers store the data.
3. How to read and write session data.
4. What kinds of values you can store.
5. How to change the ID with `regenerate()` and end a session with `destroy()`.
6. Where `Astro.session` and `context.session` are available.
7. How to turn sessions off and add types.

## What Sessions Are

### Sessions vs Cookies

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-sess" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-sess">Animation: the browser keeps only a small session ID cookie. Each request carries the ID to the server, which looks up the visitor's data in its store.</title>
  <defs><marker id="ss-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="20" y="50" width="170" height="110" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="105" y="74" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Browser</text>
  <rect x="38" y="92" width="134" height="30" rx="6" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-blue)" }} />
  <text x="105" y="112" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 11 }}>astro-session=k7x2…</text>
  <text x="105" y="145" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>only the ID</text>
  <rect x="440" y="30" width="180" height="150" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} />
  <text x="530" y="54" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Server session store</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fill: "var(--ink)" }}>
    <text x="456" y="84">k7x2… → recent: [3 posts]</text>
    <text x="456" y="104">k7x2… → cart: 2 items</text>
    <text x="456" y="134" style={{ fill: "var(--ngc-slate)" }}>p9q1… → (another visitor)</text>
  </g>
  <path d="M190 95 H436" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#ss-arrow)" />
  <path d="M440 125 H194" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#ss-arrow)" />
  <text x="315" y="86" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>request carries the ID</text>
  <text x="315" y="144" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>page built from the data</text>
  <circle r="6" cy="95" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="190;436" dur="2.5s" repeatCount="indefinite" /></circle>
  <circle r="6" cy="125" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="440;194" dur="2.5s" begin="1.25s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>The browser holds the key. The server holds the box.</figcaption>
</figure>

Use a **cookie** when:

- the data is small and not secret, such as a theme,
- browser JavaScript may need to read it.

Use a **session** when:

- the data is private, such as who is logged in,
- the data is large, such as a shopping cart,
- the visitor must not be able to change it.

Press **▶ Run**, then click **Again** a few times. Both counters work, but read the **Terminal**: the cookie carries the number itself, the session cookie only carries an ID.

<p>Cookie counter: {inCookie} (the browser holds the number)</p>
<p>Session counter: {inSession} (the server holds the number)</p>
<a href="/">Again</a>
`,
  }}
/>

### A Session Is Created on First Use

- Astro does not make a session for every visitor.
- It makes one the first time your code calls `set()`.
- Visitors who never need one never get a cookie.

Reading a session that does not exist yet is safe: `get()` just returns `undefined`. Only `set()` creates one.

**Simple lesson:** small, harmless data can be a cookie. Private or large data belongs in a session.

## Configuring a Session Driver

### What a Driver Is

- **Session driver** = where the server keeps session data: files, memory, Redis, a cloud store.
- The data must survive between requests, so the driver matters.

### Default Drivers

- The **Node** adapter stores sessions in files on the server by default.
- The **Cloudflare** adapter uses Cloudflare KV. The **Netlify** adapter uses Netlify Blobs.
- With other adapters, such as Vercel, choose a driver yourself.

### Choosing a Driver with sessionDrivers

```js
import { defineConfig, sessionDrivers } from "astro/config";
import vercel from "@astrojs/vercel";

export default defineConfig({
  adapter: vercel(),
  session: {
    driver: sessionDrivers.redis({ url: process.env.REDIS_URL }),
    ttl: 60 * 60 * 24, // seconds until an unused session expires
  },
});
```

- `ttl` = "time to live": how long an unused session lasts.
- `session.cookie` changes the cookie's name or options.

### Configuring a Driver at Runtime (Advanced)

- Some drivers need values that only exist on the live server, such as a secret URL.
- Read them from environment variables when the server starts, not from code in the repository.

In this lab, sessions live in memory inside the preview and are lost when you press Run again. On a real server, the driver keeps them in files or a database.

**Simple lesson:** the adapter usually picks a driver for you. Choose one with `sessionDrivers` when it does not.

## Reading and Writing

### get() and set()

- `await Astro.session.get("key")` returns the value, or `undefined`. It always needs `await`.
- `Astro.session.set("key", value)` saves a value. No `await` is needed.
- `Astro.session.delete("key")` removes one value.

### What You Can Store

- Text, numbers, `true`/`false`.
- Arrays and plain objects.
- `Date`, `Map`, `Set` and `URL` objects. They come back as the same type.
- **Not** functions or class instances with methods. Store their data instead.

Press **▶ Run**. Add items to the cart. The cart is a `Map` in the session.

<form method="post">
    <button name="item" value="Astro sticker">Add Astro sticker</button>
    <button name="item" value="CSS sticker">Add CSS sticker</button>
</form>
<h2>Cart</h2>
<ul>{[...cart].map(([item, count]) => <li>{item} × {count}</li>)}</ul>
{updatedAt && <p><small>Updated at {updatedAt.toLocaleTimeString("en-IN")}</small></p>}
`,
  }}
/>

- `updatedAt` came back as a real `Date`, so `toLocaleTimeString()` works.

**Simple lesson:** `await get()` to read, `set()` to write. Plain data, dates, maps and sets are all fine.

## Changing and Ending Sessions

### regenerate()

- `await Astro.session.regenerate()` keeps the data but gives the session a **new ID**.
- Call it when someone logs in. An attacker who knew the old ID can no longer use it.
- This protects against **session fixation**: tricking a visitor into using an ID the attacker chose.

### destroy()

- `Astro.session.destroy()` deletes the session data and the cookie.
- Call it when someone logs out.

### Using Sessions in Pages and Elsewhere

- In pages: `Astro.session`.
- In endpoints, actions and middleware: `context.session`.
- Only in **on-demand** routes. A static page is built before any visitor exists, so it has no session.

Press **▶ Run**. Watch the session ID in the **Terminal** change when you press "Pretend to log in", and disappear on "Log out".

<p>Visits counted in this session: {visits}</p>
<form method="post">
    <button name="step" value="visit">Count a visit</button>
    <button name="step" value="login">Pretend to log in</button>
    <button name="step" value="logout">Log out</button>
</form>
`,
  }}
/>

### Sessions in Endpoints

- An endpoint reads the same session with `context.session`.

Press **▶ Run**. Add a sticker on the page, then click the link to the endpoint. It reads the same session.

<a href="/api/cart.json">See the cart as JSON</a>
`,
    "src/pages/api/cart.json.ts": `export async function GET({ session }) {
    const cart: string[] = (await session?.get("cart")) ?? [];
    return Response.json({ items: cart.length, cart });
}
`,
  }}
/>

### Disabling Sessions

- Set `session: false` in `astro.config.mjs` to turn them off completely.
- Then `Astro.session` is `undefined`.

### Sessions and Edge Middleware

- Some hosts run middleware on the **edge**: small servers close to the visitor.
- Edge middleware may not reach your session driver. Check your host's guide before using sessions there.

### [TS] Typing App.SessionData

- Describe your session keys in `src/env.d.ts`:

```ts
declare namespace App {
  interface SessionData {
    user: string;
    cart: Map<string, number>;
    recent: string[];
  }
}
```

- Now `await Astro.session?.get("cart")` is typed as `Map<string, number> | undefined`.
- **[JS]** You do not need this. The values come back the same.

Always `regenerate()` the session after a login, and `destroy()` it on logout. Reusing the same ID across a login is a classic security hole.

**Simple lesson:** `regenerate()` on login, `destroy()` on logout. Sessions work only in on-demand routes.

## TL;DR

1. **Session** = per-visitor data on the server, linked by an `astro-session` ID cookie.
2. **Cookie vs session** = small public data in a cookie, private or large data in a session.
3. **Driver** = where sessions are stored. Node, Cloudflare and Netlify adapters pick one. Others: `sessionDrivers`.
4. **Created on first use** = the first `set()` makes the session.
5. **Read** = `await session.get(key)`. **Write** = `session.set(key, value)`. **Remove** = `session.delete(key)`.
6. **Values** = text, numbers, arrays, objects, `Date`, `Map`, `Set`, `URL`.
7. **regenerate()** = new ID, same data (on login). **destroy()** = all gone (on logout).
8. **Where** = `Astro.session` in pages, `context.session` elsewhere. On-demand only.
9. **Off** = `session: false`.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `session.get()` without `await` | It returns the value | `await` it | You get a Promise, not the value |
| Using a session on a static page | Every page has one | Use on-demand routes | `Astro.session` is `undefined` |
| No `regenerate()` after login | The same ID is fine | Regenerate on login | Session fixation attacks |
| Storing a class instance | Any object works | Store plain data | Methods are lost |
| Changing an object without `set()` | Changes save themselves | Call `set()` again after changing | The change is lost |
| No driver on a host without a default | Astro picks one | Configure `session.driver` | Sessions fail on the live site |

</div>

## Check Yourself

## Exercises

## What to Remember

- A session keeps data on the server. The browser only holds the `astro-session` ID.
- The adapter usually picks the storage driver. Choose one with `sessionDrivers` when needed.
- `await get()`, `set()`, `delete()`. Dates, maps, sets and plain data are kept.
- `regenerate()` on login, `destroy()` on logout.
- Sessions only work in on-demand routes.

Next: **Actions and Forms**, where forms call type-safe server functions.
