# Sign-in and users

> How midcode reads your site's sign-in from its code, lists the people in your database without ever reading a password, and what it asks your agent to build.

- Page: https://midcode.app/docs/data/users
- From the midcode docs. Every page as Markdown: https://midcode.app/llms.txt
- Status: This ships with the next release of midcode. The version you can download today (1.1.2) doesn't have it yet.
- Beta: midcode labels this beta.

The **Sign-in** group of [Database](https://midcode.app/docs/data/database.md) shows who signs in to your site and how: the people, the ways to sign in the site offers, and which pages ask for a signed-in visitor.

Two things to know first. midcode reads all of this from your project's code and never guesses it from the database alone. And midcode **does not write sign-in code**: adding sign-in, another way to sign in, or a check on a page is put in words for your agent, with what midcode already knows about the project.

## Where it is

Open **Database** in the top bar. Under **Sign-in** in the list on the left there are three places: **People**, **Ways to sign in** and **Protected pages**. The last two show a count once a sign-in library is found.

With no sign-in in the project, People says "No sign-in yet" and offers **Add sign-in**.

## What's read from your code

### The library or service

midcode looks at your `package.json` (and, for three frameworks, at their files). The first that matches, in this order:

| Sign-in | Recognised by |
| --- | --- |
| Auth.js | `next-auth`, `@auth/core`, `@auth/sveltekit`, `@auth/nextjs` or `@auth/express` |
| Better Auth | `better-auth` |
| Clerk | Any `@clerk/…` package |
| Supabase Auth | A Supabase package, and code that calls its auth (`.auth.signInWithPassword(`, `.auth.getUser(`, `.auth.signOut(`…) |
| Firebase Auth | `firebase`, and an import from `firebase/auth` |
| Auth0 | `@auth0/nextjs-auth0` or `@auth0/auth0-react` |
| Kinde | Any `@kinde-oss/…` package |
| WorkOS | `@workos-inc/authkit-nextjs` or `@workos-inc/node` |
| Lucia | `lucia` |
| Laravel | An `artisan` file |
| Django | A `manage.py` file |
| Devise | `devise` in the `Gemfile` |

Supabase is often only the database, so its package alone doesn't count: the code has to call its auth.

### Ways to sign in

Read from the library's own setup, for three of them:

| Library | What midcode reads |
| --- | --- |
| Auth.js | The providers imported from `next-auth/providers/…` or `@auth/…/providers/…` |
| Supabase Auth | `signInWithOAuth({ provider: '…' })`, `signInWithPassword` / `signUp` (email and password), `signInWithOtp` (email link) |
| Better Auth | The keys of `socialProviders`, `emailAndPassword: { enabled: true }`, `magicLink()`, `passkey()` |

Each way shows the file it's set up in; click it to open the file. When the users are in your database, each also shows how many people use it, and a way people use that the code doesn't show (one set up in the service's dashboard) is listed too.

For the other libraries nothing is read from the code. With a hosted service the list says so: "They're set up in Clerk, not in your code."

### Protected pages

Every [page of the site](https://midcode.app/docs/editor/pages.md), with a lock when a check for a signed-in visitor was found. There are two kinds:

- **"Checked before the page, in `proxy.ts`"**: Next.js runs one file before pages (`proxy.ts` since Next 16, `middleware.ts` before; also under `src/`). midcode reads its `matcher` and whether the file asks for a session at all. A page counts when a matcher covers its route, or when the file has no matcher.
- **"Checked in `app/account/page.tsx`"**: the page's own file, or in the App Router any layout above it, both asks who is signed in (`auth()`, `getServerSession()`, `currentUser()`, `getUser()`, `getSession()`, `requireUser()`…) and turns the visitor away (`redirect()`, `notFound()`, `unauthorized()`, `forbidden()`, `redirectToSignIn()`, `auth.protect()`).

Anything else says **No check found**.

> [!WARNING]
> This is reading code, not running it. A lock means a check is there, not that it holds; "No check found" means exactly that, not that the page is open. Try the page signed out to be sure. The view says so too.

## People

People lists the rows of the table your sign-in keeps users in.

- With Supabase Auth, that's `auth.users`.
- Otherwise it's the first of `User`, `user`, `users`, `Users`, `auth_user`, `accounts_user` or `profiles`, in the default schema, that has an email column and a primary key.

Columns are recognised by name: email, name, picture (`image`, `avatar`, `avatar_url`, `picture`…), whether the email is confirmed, when the person joined, when they were last seen, and a role (`role`, `tier`, `plan`, `is_admin`…). Only those columns are asked for. A page of people is one query:

```sql
select "id", "email", "name", "created_at", "password_hash" is not null
from "public"."users"
order by "created_at" desc nulls last, "id"
limit 50 offset 0
```

**midcode never reads a password's hash or a token.** For a password column it asks only whether it's set (`is not null`), to show that the person signs in with a password. The other ways each person uses come from the accounts table beside the users (`account`, `accounts` or `identities`, with a user id and a provider).

Each row shows the person's picture or initial, name and email, a check if the email is confirmed, the role, an icon per way to sign in, and "Joined" and "Seen" dates. Search by name or email; the list is 50 people at a time, newest first.

Click a person to open their row in its table, filtered to that row. There it's a row like any other: see [Edit rows](https://midcode.app/docs/data/database.md).

Reading people needs no permission to change the database, so it works on a read-only connection.

### When the users aren't in your database

Clerk, Firebase Auth, Auth0, Kinde and WorkOS keep your users for you. People then says "Your users are in" that service, with a button that opens its dashboard. If the database has no table of users, it says "No table of users here" and asks whether it's the right connection.

## What the agent is asked

Three buttons open a dialog that ends in a text for [your agent](https://midcode.app/docs/agents/overview.md). Each has two steps: you choose, then you read exactly what will be asked, and **Copy** it or **Send to the agent**. The files the agent changes from the Chat view are undoable and show in [Publish](https://midcode.app/docs/publish/publish.md).

| Button | Where | You choose |
| --- | --- | --- |
| **Add sign-in** | Overview, and People when there's none | The library, the ways to sign in, and (optional) the pages only signed-in people see |
| **Add a way to sign in** | Ways to sign in | Which of email and password, Google, GitHub and email link to add (the ones the site doesn't have) |
| **Protect pages** | Protected pages; **Protect…** on a page's row | The pages, from those with no check found |

For **Add sign-in**, midcode offers Supabase Auth, Better Auth, Auth.js and Clerk, and marks one "Suggested": Supabase Auth if the project already uses Supabase, Better Auth if a database is connected, Clerk if there's none.

Every brief starts from what midcode read, so the agent doesn't have to find it again: the framework and its version, the router, TypeScript or JavaScript, the package manager, Tailwind, the ORM, the database's engine, the sign-in already there with its config file, and how the site protects other pages already. A brief for a Next.js project with Prisma:

```text
Add sign-in to this site with Better Auth.

The project: Next.js 16.0.1, app router, TypeScript, pnpm, Tailwind, Prisma for the database. Its database is Postgres.

What to build:
- Ways to sign in: Email and password, Google.
- A sign-in page at /sign-in and a sign-up page at /sign-up, looking like the rest of the site: reuse its components and styles, no new UI library.
- A way to sign out, and who is signed in shown in the site’s header or nav if it has one.
- Require sign-in for these pages, sending signed-out visitors to /sign-in and back where they were going afterwards:
    /account (app/account/page.tsx)

How:
- Use Better Auth, keeping users in the project's own database. Its tables (user, session, account, verification) are added through Prisma: with the Prisma adapter, add the models to prisma/schema.prisma and apply them the way this project applies schema changes.
- In Next 16 the file that runs before pages is proxy.ts (middleware.ts before 16): use the one this project's version (16.0.1) expects.
- Keys and secrets go in .env.local as placeholders, and their names in .env.example. Never make up a real key, and never commit one.
- When you’re done, list what’s left to do by hand: each key to create, where to create it, and the variable it goes in.
```

The keys the agent leaves as placeholders show up afterwards in [Environment variables](https://midcode.app/docs/data/env.md) as missing, ready to be given a value.

A "protect" brief also names the file that runs before pages and its matchers, and points at a page that's already protected: "Do it the way the site already does".

## Limits

- Sign-in and users is in beta and not in the released version yet.
- midcode writes no sign-in code, and has no actions on users: no invite, no password reset, no block. A person's row can be edited as a row, nothing more.
- Ways to sign in are read for Auth.js, Supabase Auth and Better Auth only.
- The file that runs before pages is read for [Next.js](https://midcode.app/docs/frameworks/nextjs.md). Checks made elsewhere (SvelteKit hooks, Nuxt or Laravel route middleware, a Django decorator, a wrapper component) aren't recognised: those pages say "No check found".
- A matcher is read when it's written as strings. With one built in code, or a file that decides by its own `if`s instead of a matcher, midcode takes the file to run on every page, so every page shows as checked.
- Users kept by a hosted service (Clerk, Firebase Auth, Auth0, Kinde, WorkOS) aren't listed; midcode links to the service.
- The users table is found by its name. A table called something else isn't found: open it from **Tables**.
- A picture is shown only when it's a full `http(s)` address.
- What was checked: the reading of code against `package.json` files and source files of each kind, and the People list against users seeded in a test Postgres. Not tried against live projects of each service.
