Skip to Content
Vektos
Guides

Sign-in and saved conversations

Who may use the chat, how they sign in with GitHub, what is stored about them, and how a conversation is saved and read back.

What

The dashboard is public. The chat is not: it calls a paid model, so it needs a GitHub sign-in, and only the GitHub usernames on an allowlist may use it. Each person has their own conversations, saved on the server, listed beside the chat, and their own spending cap.

# In Infisical (or the environment): GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET,
# BETTER_AUTH_SECRET (openssl rand -base64 32), VEKTOS_ALLOWED_GITHUB=vallo87
bun run db:migrate
infisical run -- bun run web            # http://localhost:3000/chat → Sign in with GitHub
PieceWhereJob
Better Authapps/web/src/lib/auth.ts, app/api/auth/[...all]GitHub OAuth, sessions in Postgres
Allowlistpackages/chat/src/access.tsVEKTOS_ALLOWED_GITHUB, comma-separated usernames
Conversationspackages/chat/src/conversations.tsCreate, own, list, load, save, delete
Pagesapp/chat/page.tsx, app/chat/[id]/page.tsx, components/chat/shell.tsxThe checks, then the list and the chat

The GitHub OAuth app's callback is http://localhost:3000/api/auth/callback/github. In production it is the deployed address, set in GitHub and in BETTER_AUTH_URL.

Why

  • Only the chat is behind sign-in. The dashboard is the demo; anyone may read it. The chat spends money, so it needs to know who is asking.
  • GitHub, with an allowlist. The people who will use it have GitHub accounts, and the username is a stable, readable name to list. The list is checked twice:
    1. When an account would be created. A Better Auth hook refuses a new user whose username is not listed, so a stranger who signs in leaves nothing in the database.
    2. On every request. The chat compares the session's username with the list each time, so a name taken off the list stops working at once. A session hook also refuses a new session for a user no longer listed.
  • Better Auth, not a homemade session. Sessions, the OAuth state, CSRF on the auth routes and cookie handling are the parts where a small mistake is a security hole. Better Auth stores them in our Postgres through Drizzle, with tables we generated with its CLI and own as migrations.
  • Nothing to change about yourself. Only GitHub sign-in is on: the email and password routes and /update-user are disabled, so the stored username is always the one GitHub sent.
  • The GitHub token is encrypted. Better Auth keeps the OAuth access token; Vektos never uses it. encryptOAuthTokens stores it encrypted with the server's secret.
  • The conversation id is the browser's, ownership is the server's. The page makes a UUID for a new conversation so it can show it at once. The first request with that id creates it for the signed-in person; a request with someone else's id is answered as if the conversation did not exist (404), so nobody can find out what ids are in use.
  • The browser sends one question. The server reads the history from chat_messages and saves each answer as the AI SDK's UI message, tool results included. A follow-up uses those results without calling the tools again, and a browser cannot slip a made-up result into the history.
  • A cap per person. One person cannot use the whole chat's budget: $0.50 each by default, inside the $1 for everyone.
  • No title model. A conversation's title is its first question, cut at a word. A model call to name it would cost money for nothing a reader needs.

Try it

bun test packages/chat/src/access          # the allowlist
bun run check:db                           # conversations, ownership, the endpoint's checks
infisical run -- bun run web

Open /chat, sign in, ask, reload: the conversation is still there, and listed on the left. Open another browser profile, sign in with an account that is not on the list: you are sent back with "Sign-in did not complete", and auth_user has no row for it.

What the first real sign-in stored, checked in the database: one user, vallo87; one session, valid for seven days; a GitHub token stored encrypted (no gho_ prefix); one conversation titled with its first question, two messages whose parts include the results of explain_rule and search_docs; and its three costs, each with the user and the conversation.

What is stored, and how to remove it

TableWhatRemoved when
auth_userGitHub id (as the account), name, email, avatar URL, usernameby hand (see below)
auth_sessiona token, when it expires, the IP address and the browser's user agentsign-out, or expiry after 7 days
auth_accountthe link to GitHub, the token (encrypted) and its scopeswith the user
chat_conversations, chat_messagesthe questions and answers, with tool results"Delete conversation", or with the user
chat_usagecost of each model call, with the user and conversationkept: the user and conversation become empty when deleted

There is no "delete my account" button yet. To remove a person, delete their auth_user row: their sessions, account link, conversations and messages go with it, and their costs stay without a name.

Extend it

  • Another person. Add their GitHub username to VEKTOS_ALLOWED_GITHUB and restart the app.
  • Another provider. Add it to socialProviders in lib/auth.ts, and decide what the allowlist compares (the allowlist reads githubLogin today).
  • A deployed app. Set BETTER_AUTH_URL to its address, add that callback in the GitHub OAuth app, and use a new BETTER_AUTH_SECRET; tokens encrypted with the old one cannot be read.

Known gaps

  • The username is read once. It is stored when the account is created. Someone who renames their GitHub account keeps the old name here until their user row is deleted.
  • No account deletion in the interface, and no export of one's conversations.
  • No rate limit beyond the caps. A listed person can ask as fast as they like until their cap.
  • Better Auth's own routes are not covered by our tests. The tests drive the chat with a made-up user; the OAuth flow was tried by hand, once, with the real GitHub app.
  • Stored answers include the model's reasoning parts. They are not shown, but they are kept.

On this page