build

Application-Aware Session Context in Astro

Part of Villa Owner Trust System

While building authentication for the Villa Owner Trust System, I found that a Better Auth session did not contain enough information for the application to make business-level access decisions.

Better Auth could identify the signed-in user, but protected admin and owner requests also needed to know what that user represented inside the application:

Better Auth user and session
active application user
application role
management company
linked owner profiles

Those relationships belong to the application’s own PostgreSQL model rather than the Better Auth session.

I built a request-level SessionContext to connect those two layers.

For each relevant request, session.ts reads the Better Auth session, resolves the corresponding application access records through Drizzle, and returns one typed context object. Astro middleware stores that context in context.locals, making the same data available to pages, Actions, endpoints, and authorization code.

Better Auth identity and application access

Better Auth owns authentication:

user identity
login
logout
session
auth-owned tables

The Villa Owner Trust System owns its business access model:

application user
role
management company
owner profile links
property access
record visibility
proof visibility
statement access
export access

A valid Better Auth session therefore establishes identity, but it does not by itself grant access to the application’s business records.

The first application-level requirement is an active app_users record linked to the Better Auth user.

The relationship begins as:

Better Auth user + session

            │ auth user ID

        app_users

            ├── role
            ├── management company
            └── owner profile links

Keeping those models separate lets Better Auth remain responsible for authentication while the application owns the relationships and rules that determine business access.

SessionContext

The shared request context currently has this shape:

export type SessionContext = {
  user: AuthSession["user"] | null;
  session: AuthSession["session"] | null;
  appUser: AppUserSessionContext | null;
  role: AppUserRole | null;
  managementCompanyId: string | null;
  ownerProfileIds: string[];
};

Those values establish the base information needed by protected application code:

Is there a Better Auth session?
Is there an active application user?
What application role does the user have?
Which management company are they part of?
Which owner profiles are linked to them?

The context deliberately stops there.

It does not decide whether the user may access a particular property, expense, proof document, statement, or export. Those decisions belong to more specific authorization rules built on top of this request context.

Building the context in session.ts

session.ts connects the Better Auth session to the application’s Drizzle-backed access model.

The flow is:

read Better Auth session

find active app_users record

load role and management company

load active owner profile links

return SessionContext

The app_users lookup is an important boundary.

A user can have a valid Better Auth identity and session without having active access to the Villa Owner Trust System. Authentication therefore does not bypass the application’s own access records.

The Better Auth session type comes from the configured Better Auth instance:

type AuthSession = typeof auth.$Infer.Session;

AuthSession contains the inferred user and session types produced by the actual Better Auth configuration.

The application-user and role types come from the Drizzle schema:

export type AppUser = typeof appUsers.$inferSelect;
export type AppUserRole = AppUser["role"];

That keeps the TypeScript definitions tied to Better Auth and the Drizzle schema instead of redefining their shapes independently.

Login users and owner profiles remain separate

The Better Auth user is not the owner profile.

They represent different things:

Better Auth user
    a login identity

Owner profile
    a villa owner in the business model

The application connects them through its own access records:

Better Auth user

   app_users

owner profile link

 owner profile

The session context currently exposes the linked owner profile IDs:

ownerProfileIds: string[];

It does not place owner information directly on the Better Auth user.

That separation allows the authentication model and ownership model to evolve independently. It also avoids assuming that business access will always remain a one-login-to-one-owner relationship.

For example, the model can support relationships such as:

one owner profile → multiple login users

one login user → multiple owner profiles

without changing Better Auth’s identity model.

Property access remains separate again. Being linked to an owner profile does not automatically grant access to every property.

Exposing SessionContext through Astro middleware

Astro middleware loads the session context for the relevant request surfaces and writes its values to context.locals.

The request path becomes:

request

Astro middleware

session.ts

SessionContext

context.locals

Astro page / Action / endpoint / authorization code

The middleware assigns:

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;

Astro pages can then read the same values through:

Astro.locals

API endpoints and Actions receive them through their request context.

This prevents each protected route or action from independently resolving the Better Auth session and application access records.

Route-aware context loading

The application does not resolve SessionContext for every request.

The middleware currently loads it for the route surfaces that need authentication or application access context:

/
/login
/admin/*
/owner/*
/api/files/*
/api/exports/*
/api/uploads/*

Better Auth’s own API routes are excluded:

/api/auth/*

Those routes are handled by the Better Auth handler itself and do not need the application’s session-context lookup wrapped around them.

The route matching helper is:

function isPathOrChild(pathname: string, basePath: string): boolean {
  return pathname === basePath || pathname.startsWith(`${basePath}/`);
}

That keeps the route boundary explicit:

/admin              → matches
/admin/properties   → matches
/admin-tools        → does not match

A request therefore receives application session context because it belongs to one of the intended route trees, not merely because its pathname begins with similar characters.

Typing Astro locals in env.d.ts

Astro’s App.Locals type is declared from the existing SessionContext:

type SessionContext = import("./lib/auth/session").SessionContext;

declare namespace App {
  interface Locals extends SessionContext {}
}

env.d.ts does not load the session or run authentication code.

It gives TypeScript the shape of the values that middleware places in context.locals and that Astro exposes through Astro.locals.

Importing SessionContext also avoids maintaining a second locals definition.

The relationship remains:

session.ts
    defines SessionContext


env.d.ts
    extends App.Locals


Astro middleware, pages,
Actions, and endpoints
    use the same types

Where authorization begins

After the middleware has populated Astro locals, protected application code starts with:

user
session
appUser
role
managementCompanyId
ownerProfileIds

Those values establish identity and application context.

More specific authorization can then answer questions such as:

Does this owner have access to this property?
Is this record owner-visible?
Can this owner view this proof document?
Can this user download this statement?
Does this admin belong to the record's management company?

The responsibilities therefore remain separate:

Better Auth
    authenticates the user

SessionContext
    resolves application access context

Authorization rules
    decide access to a specific resource or operation

SessionContext is not the authorization system.

It is the request-level foundation that connects Better Auth to the Villa Owner Trust System’s Drizzle-backed access model and makes that context consistently available through Astro.