---
title: "Private apps"
description: "Put a login in front of an app and decide who gets in."
canonical_url: "https://wervt.app/guides/private-apps"
---
# Private apps

> Put a login in front of an app and decide who gets in.

Apps are **private by default**. Before anything of a private app is served, the runtime checks
that the visitor is signed in and on the app's list. Nothing runs in the app's isolate for anyone
else.

```bash
wervt access my-app                                   # show visibility and list
wervt access my-app --add friend@example.com,*@company.com
wervt access my-app --remove friend@example.com
wervt access my-app --public                          # anyone, no login
wervt access my-app --private
```

Admins (`WERVT_ADMIN_EMAILS` on the runtime) can open every private app.

## How it works

1. A visitor without a session is redirected to `auth.wervt.app`, which offers GitHub, Discord,
Twitch and passkeys (powered by [better-auth](https://better-auth.com)).
2. After login the auth app sets a short-lived signed `wervt_session` cookie on `.wervt.app`, so one
login covers every private app.
3. The runtime verifies that cookie locally (HMAC, no network call), checks the list and forwards the
request. It strips the cookie first, so app code can never read or replay it.
4. Signed in but not on the list? The visitor sees a "No access" page with the command to add them.

Access is managed by the control plane, not by app code: an agent editing an app can't make it
public.

## The user in your app

Private apps always receive the signed-in user. Public apps receive one when the visitor is signed
in, so a public app can still have optional accounts, admins or "Twitch users only" features.

<code-group>

```ts [server/api/me.get.ts]
import { useWervtUser } from '@wervt/nuxt/server'
import { defineEventHandler } from 'nuxt/server'

export default defineEventHandler((event) => useWervtUser(event))
// { id, email, name?, image?, accounts?: { github?: '…', twitch?: '…' } }
```

```vue [app.vue]
<script setup lang="ts">
const user = useWervtUser() // auto-imported, ready before the first render
</script>

<template>
  <p v-if="user">Hi {{ user.name ?? user.email }}</p>
</template>
```

</code-group>

During SSR the user travels in the payload. Apps without SSR (`ssr: false`) load it from
`/_wervt/user` before the app renders, so `useWervtUser()` is set either way.

Use `user.id` to scope data, e.g. `proxyShape(event, { table: 'notes', where: '"ownerId" = $1', params: [user.id] })`.

`accounts` maps each login provider the user linked to their id there, e.g. check
`user.accounts?.twitch` before letting someone into a Twitch-only feature.

### Sign in and out from an app

Every app host has `/_wervt/login` and `/_wervt/logout`. Link to them with a full page load (not a
client-side navigation); `redirect` is a path in your app to come back to:

```vue
<UButton label="Sign in" :to="wervtLoginUrl(route.fullPath)" external />
<UButton label="Sign out" :to="wervtLogoutUrl('/')" external />
```

`wervtLoginUrl` and `wervtLogoutUrl` are auto-imported. Signing out signs out of all wervt apps.

In `nuxt dev` there's no wervt login: `/_wervt/login` signs you in as a dev user
(`dev@wervt.localhost`, with a Twitch account linked). Change it with the module option
`wervt: { dev: { user: { … } } }`.

## Sessions for your own data

For state of your own (which room a browser joined, a cart, anonymous visitors), use the session
from `nuxt/server`. wervt gives every app its own `NUXT_APP_SECRET`, which Nuxt derives the session
key from, so there's nothing to configure:

```ts
import { useSession } from 'nuxt/server'

const session = await useSession<{ rooms?: string[] }>(event, { name: 'my-app' })
await session.update({ rooms: [...(session.data.rooms ?? []), roomId] })
```

## Secrets for apps

`wervt env` sets environment variables for an app. Values are write-only: reading lists only the keys.

```bash
wervt env my-app --set API_KEY=… --set OTHER=…
wervt env my-app --unset OTHER
```

Names starting with `WERVT_` and `DATABASE_URL` are reserved for the runtime. Changes apply from the
next request, which starts a fresh isolate; no redeploy needed. Access changes apply immediately too.


## Sitemap

See the full [sitemap](https://wervt.app/sitemap.md) for all pages.
