Auth & JWT Shape

Understanding the JWT structure is required for any feature that reads session data, enforces permissions, or scopes queries to the correct tenant.

Login identifier

pb_users uses the login column as the authentication identifier — not email. This is the field checked against the submitted password.

-- Correct auth lookup
SELECT id, login, id_company, role, password_hash
FROM pb_users
WHERE login = :login
  AND id_company = :id_company
  AND deleted_at IS NULL;

Never query by email for authentication purposes — the email column exists on the record but is not the auth identifier.

JWT payload shape

After successful login, a signed JWT is issued. The payload:

{
  "sub":        "42",
  "login":      "jane@agency.com",
  "id_company": 7,
  "role":       "admin",
  "iat":        1700000000,
  "exp":        1700086400
}
Field Type Description
sub string User ID (pb_users.id) as a string
login string The login identifier
id_company number Tenant ID — use this for all query scoping
role string admin, member, or viewer
iat number Issued at (Unix timestamp)
exp number Expiry — default 24 hours from issue

Reading the session in Next.js

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

const session = await getServerSession(authOptions);

if (!session) {
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { id_company, role } = session.user;

session.user mirrors the JWT payload. All fields listed above are available directly.

The cardinal rule

Always take id_company from session.user — never from request params, query strings, or request bodies.

// Wrong — user can send any id_company they want
const id_company = req.body.id_company;

// Correct — verified by the JWT signature
const id_company = session.user.id_company;

Role enforcement

function requireAdmin(session: Session) {
  if (session.user.role !== 'admin') {
    throw new Error('Admin access required');
  }
}

Roles:

Role Can do
admin Full read/write, settings, team management
member Read/write contacts, deals, companies
viewer Read only — no mutations

Token expiry and refresh

JWTs expire after 24 hours. There is no refresh token — expired sessions redirect to /login. If you are building a long-running background job or API integration, use an API key instead of a JWT. See the Authentication docs for API key setup.