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.