Owner Trust Portal Session Context with Astro, Better Auth, and Drizzle
A villa owner trust portal needs more than a normal login check.
A login session can confirm who is signed in, but the product also needs to understand what that person is allowed to represent inside the portal:
Which management company does this user belong to?
Is this user an admin, staff member, or owner?
Is the app access active?
Which owner profile is connected to this login?
Which future property access rules should apply?
That is the purpose of the session context layer.
It connects Better Auth’s session data to the app-owned access model stored in PostgreSQL through Drizzle.
The result is a request-level context that Astro pages, actions, endpoints, and future authorization guards can use consistently.
Product context
The Owner Trust Portal is built around proof-backed reporting for villa owners.
Owners will eventually view assigned villas, bookings, expenses, maintenance records, proof documents, payouts, and monthly statements.
That makes access context a core product concern.
The app cannot treat every signed-in user the same way. A management admin, staff member, and villa owner each need different access rules.
Better Auth handles the identity layer:
user identity
login
logout
session
auth-owned tables
The application handles the business access layer:
app user role
management company scope
owner profile linkage
property access
record visibility
proof visibility
statement access
export access
This separation keeps authentication and product authorization clean.
Session context shape
The session context gives each relevant request a consistent base shape:
export type SessionContext = {
user: AuthSession["user"] | null;
session: AuthSession["session"] | null;
appUser: AppUserSessionContext | null;
role: AppUserRole | null;
managementCompanyId: string | null;
ownerProfileIds: string[];
};
Those fields answer the first set of app-level questions:
Is there a signed-in user?
Is there an active app user?
What role does the app user have?
Which management company owns this context?
Which owner profiles are linked to this user?
That context becomes the base for later route guards and authorization checks.
session.ts
The session.ts file creates the app-aware session context.
The process is:
Read the Better Auth session from the request
→ find the active app user linked to that auth user
→ load role and management company scope
→ load active owner profile links for owner users
→ return one typed context object
The important design choice is that Better Auth user data is not treated as enough to access business records.
A user also needs an active app_users record.
That keeps the login identity separate from the portal’s business access model.
The session type is inferred from the actual Better Auth instance:
type AuthSession = typeof auth.$Infer.Session;
That keeps the local TypeScript types aligned with the configured auth system.
The app user type comes from the Drizzle schema:
export type AppUser = typeof appUsers.$inferSelect;
export type AppUserRole = AppUser["role"];
That keeps the role type connected to the database model instead of redefining role values manually.
Owner profile linkage
Owner access is not stored directly on the Better Auth user.
The app uses a separate owner profile model because a login user and an owner business profile are not the same thing.
That distinction matters for this product.
An owner profile represents ownership and contact context. A login user represents someone who can authenticate into the portal.
This allows the model to support future cases such as:
one owner profile with more than one login user
one user connected to more than one owner profile
future co-owner or advisor access
future accountant access
For now, the session context only loads the linked owner profile IDs.
Property-level access is handled later through property access rules.
middleware.ts
Astro middleware connects the session context to each request.
The middleware initializes context.locals, loads the session context when needed, and makes the result available to pages and endpoints.
The request flow is:
Request enters Astro
→ middleware runs
→ session context is loaded for protected app surfaces
→ context is stored in Astro locals
→ pages and endpoints read Astro.locals
The middleware stores:
context.locals.user = sessionContext.user;
context.locals.session = sessionContext.session;
context.locals.appUser = sessionContext.appUser;
context.locals.role = sessionContext.role;
context.locals.managementCompanyId = sessionContext.managementCompanyId;
context.locals.ownerProfileIds = sessionContext.ownerProfileIds;
This keeps page and endpoint code cleaner because they do not need to reload the same base context independently.
Route-aware session loading
Session context is only loaded for route surfaces that need it:
/
/login
/admin/*
/owner/*
/api/files/*
/api/exports/*
/api/uploads/*
Better Auth’s own API route is skipped:
/api/auth/*
This keeps the auth handler isolated and avoids unnecessary database work on unrelated routes.
The route matching uses a helper:
function isPathOrChild(pathname: string, basePath: string): boolean {
return pathname === basePath || pathname.startsWith(`${basePath}/`);
}
This keeps route matching precise.
For example:
/admin matches
/admin/properties matches
/admin-tools does not match
That matters because session and authorization boundaries should be explicit, not accidentally triggered by loose path prefixes.
env.d.ts
The env.d.ts file gives TypeScript the shape of Astro locals.
It does not run in the app.
It only tells TypeScript that Astro.locals and context.locals follow the same shape as SessionContext.
type SessionContext = import("./lib/auth/session").SessionContext;
declare namespace App {
interface Locals extends SessionContext {}
}
This keeps the locals type tied to one source of truth.
If the session context changes later, the Astro locals type follows automatically.
How the pieces work together
The complete flow is:
Better Auth validates the session
→ session.ts loads app-owned access context
→ middleware stores that context in Astro locals
→ pages, actions, endpoints, and guards read the same typed context
That creates the foundation for the rest of the protected app.
The core product rule stays clear:
Authentication identifies the user.
The app access model explains what that user represents inside the owner trust portal.
For an owner trust product, that distinction is essential.
A signed-in owner should eventually see only assigned properties, owner-visible records, owner-visible proof, and finalized or allowed statements.
A signed-in admin should work inside their management company scope.
A signed-in staff user should only receive the internal permissions the product allows.
The session context does not replace those later authorization rules. It gives them the correct starting point.
Result
The app now has a typed request-local context for the main portal surfaces:
user
session
appUser
role
managementCompanyId
ownerProfileIds
This is the base layer for upcoming protected routes, admin workflows, owner views, file access, exports, and statement access.