| name | data-fair-session |
| description | How to use the @data-fair/lib session management system in services that consume sessions (not login/account management). Use this skill whenever the task involves reading user identity, checking permissions, protecting routes, accessing account/organization info, or implementing authorization logic in a data-fair service -- both on the Express/Node backend and in Vue frontend components. Also use it when the user mentions session middleware, account roles, admin mode, or organization switching in a data-fair context. |
data-fair Session Management -- Consumer Guide
This skill covers how services consume sessions produced by Simple Directory (the identity provider). It does NOT cover login flows or account management -- only how a service reads, verifies, and uses session data for authentication and authorization.
Architecture Overview
Sessions are stateless JWT cookies set by Simple Directory. Consuming services never store sessions -- they verify and read them on every request. The JWT is split across two cookies for security: id_token (readable by JavaScript, contains header+payload) and id_token_sign (httpOnly, contains the signature).
Additional cookies carry context: id_token_org (active organization), id_token_dep (active department), id_token_role (switched role), i18n_lang (language).
Key Types
interface SessionState {
user?: User
organization?: OrganizationMembership
account?: Account
accountRole?: string
lang: string
}
type SessionStateAuthenticated = SessionState & Required<Pick<SessionState, 'user' | 'account' | 'accountRole'>>
interface Account {
type: 'user' | 'organization'
id: string
name: string
department?: string
departmentName?: string
}
type AccountKeys = Pick<Account, 'type' | 'id' | 'department'>
The account field is the key abstraction: it normalizes "who is currently acting" regardless of whether it's a personal user or an organization. Resources are owned by an Account; permission checks compare the session's account against the resource's owner.
The user.adminMode flag indicates a platform super-admin in admin mode -- it bypasses all permission checks.
Express Backend
Package: @data-fair/lib-express
Import paths:
import { session } from '@data-fair/lib-express/session.js'
import session from '@data-fair/lib-express/session.js'
import {
reqSession,
reqSessionAuthenticated,
reqAdminMode,
reqUser,
reqUserAuthenticated,
setReqUser,
setReqSession,
assertAccountRole,
assertAdminMode,
getAccountRole,
isAuthenticated,
} from '@data-fair/lib-express/session.js'
Initialization (at server startup)
import { session } from '@data-fair/lib-express/session.js'
session.init(config.privateDirectoryUrl)
Middleware (applied to routes)
import { session } from '@data-fair/lib-express/session.js'
app.use('/api', session.middleware())
app.use('/api/private', session.middleware({ required: true }))
app.use('/api/admin', session.middleware({ adminOnly: true }))
The middleware parses cookies, verifies the JWT via JWKS, and caches the result on the request object. It also blocks non-GET requests for pseudoSession tokens (limited API key sessions).
Reading Session in Route Handlers
After middleware has run, use the sync accessors:
app.get('/api/data', (req, res) => {
const s = reqSession(req)
if (s.user) { }
})
app.post('/api/data', (req, res) => {
const s = reqSessionAuthenticated(req)
})
app.delete('/api/admin/thing', (req, res) => {
const s = reqAdminMode(req)
})
Permission Checking
The permission model is account-based ownership. Resources have an owner: Account field. Check access with:
import { reqSessionAuthenticated, assertAccountRole, assertAdminMode } from '@data-fair/lib-express/session.js'
app.put('/api/resources/:id', async (req, res) => {
const session = reqSessionAuthenticated(req)
const resource = await db.findById(req.params.id)
assertAccountRole(session, resource.owner, 'admin')
})
assertAccountRole(session, resource.owner, ['admin', 'contrib'])
assertAdminMode(session)
const role = getAccountRole(session, resource.owner)
if (role === 'admin') { }
getAccountRole resolution order:
- Not authenticated ->
null
user.adminMode -> 'admin' (super-admin bypass)
- Target is
type:'user' matching user.id -> 'admin' (self-ownership)
- Match against
session.account -> session.accountRole
- Otherwise ->
null
Options for getAccountRole / assertAccountRole:
allAccounts: true -- check all user's org memberships, not just the currently active one
acceptDepAsRoot: true -- users in the root org (no department) can access department-scoped resources
Filtering Lists by Ownership
A common pattern for listing resources scoped to the current account:
app.get('/api/resources', async (req, res) => {
const session = reqSessionAuthenticated(req)
const query: any = {}
if (req.query.showAll === 'true') {
assertAdminMode(session)
} else {
query['owner.type'] = session.account.type
query['owner.id'] = session.account.id
if (session.account.department) {
query['owner.department'] = session.account.department
}
}
const results = await db.find(query)
res.json(results)
})
Default Owner on Resource Creation
When creating a resource, default the owner to the session's active account:
app.post('/api/resources', async (req, res) => {
const session = reqSessionAuthenticated(req)
const resource = {
...req.body,
owner: req.body.owner ?? session.account
}
assertAccountRole(session, resource.owner, 'admin')
await db.insert(resource)
})
Ownership Transfer
When changing a resource's owner, check permission on both old and new:
if (patch.owner) {
assertAccountRole(session, resource.owner, 'admin')
assertAccountRole(session, patch.owner, 'admin')
}
Synthetic Sessions (API Keys, Internal Calls)
Use setReqUser or setReqSession to create a pseudo-session from an API key or service-to-service call, bypassing normal cookie parsing:
import { setReqUser } from '@data-fair/lib-express/session.js'
app.use(async (req, res, next) => {
const apiKey = req.headers['x-api-key']
if (apiKey) {
const keyRecord = await db.apiKeys.findOne({ key: apiKey })
setReqUser(req, keyRecord.user, 'fr', keyRecord.account, keyRecord.role)
}
next()
})
Passing Session to Service Layer
Thread the session state as a parameter to service functions rather than relying on request context:
const session = reqSessionAuthenticated(req)
await updateResource(session, req.params.id, req.body)
export async function updateResource(
sessionState: SessionStateAuthenticated,
id: string,
body: any
) {
const resource = await db.findById(id)
assertAccountRole(sessionState, resource.owner, 'admin')
}
Vue Frontend
Package: @data-fair/lib-vue
Import paths:
import { createSession, useSession, useSessionAuthenticated, getAccountRole } from '@data-fair/lib-vue/session.js'
import type { Session, SessionAuthenticated, SiteInfo, Account } from '@data-fair/lib-vue/session.js'
Setup -- Plain Vue SPA
import { createSession } from '@data-fair/lib-vue/session.js'
const session = await createSession({
})
const i18n = createI18n({ locale: session.state.lang })
createApp(App)
.use(session)
.use(i18n)
.mount('#app')
Setup -- Nuxt 3 SSR
Server-side (Nitro plugin):
import { SessionHandler } from '@data-fair/lib-node/session.js'
export const session = new SessionHandler()
export default defineNitroPlugin(async () => {
const config = useRuntimeConfig()
session.initJWKS(config.privateDirectoryUrl)
})
Client-side (Nuxt plugin):
import { createSession } from '@data-fair/lib-vue/session.js'
export default defineNuxtPlugin(async (app) => {
app.vueApp.use(await createSession({
req: app.ssrContext?.event.node.req,
route: useRoute(),
}))
})
Auto-imports in nuxt.config.ts:
imports: {
presets: [{
from: '@data-fair/lib-vue/session.js',
imports: ['useSession', 'useSessionAuthenticated']
}]
}
Using Session in Components
useSession() -- returns Session with possibly-undefined user. Use for public-facing pages:
<script setup>
const session = useSession()
</script>
<template>
<div v-if="session.user.value">
Logged in as {{ session.user.value.name }}
<button @click="session.logout()">Logout</button>
</div>
<div v-else>
<button @click="session.login()">Login</button>
</div>
</template>
useSessionAuthenticated() -- returns SessionAuthenticated where user, account, accountRole are guaranteed. Use for protected pages (throws if not logged in):
<script setup>
const session = useSessionAuthenticated()
// Access current account
const accountType = session.state.account.type // 'user' | 'organization'
const accountId = session.state.account.id
const role = session.state.accountRole // 'admin' | 'contrib' | 'user'
</script>
Client-Side Permission Checks
<script setup>
import { getAccountRole } from '@data-fair/lib-vue/session.js'
const session = useSessionAuthenticated()
// Check role for a specific resource owner
const canEdit = computed(() => {
return getAccountRole(session.state, resource.value.owner) === 'admin'
})
// Super-admin check
const isSuperAdmin = computed(() => !!session.state.user?.adminMode)
</script>
Organization Switching
<script setup>
const session = useSessionAuthenticated()
// List available accounts (personal + organizations)
const accounts = computed(() => {
const items = [{ label: session.state.user.name, value: null }]
for (const org of session.state.user.organizations) {
items.push({
label: org.department ? `${org.name} / ${org.departmentName}` : org.name,
value: org.department ? `${org.id}:${org.department}` : org.id
})
}
return items
})
function onSwitch(value: string | null) {
if (!value) {
session.switchOrganization(null)
} else {
const [org, dep] = value.split(':')
session.switchOrganization(org, dep)
}
}
</script>
Session Properties Reference
The Session object returned by useSession():
| Property | Type | Description |
|---|
state | reactive(SessionState) | The raw reactive state object |
user | ComputedRef<User | undefined> | Current user (null if anonymous) |
organization | ComputedRef | Active organization membership |
account | ComputedRef<Account | undefined> | Active account (user or org) |
accountRole | ComputedRef<string | undefined> | Role in active account |
lang | ComputedRef<string> | Current language |
theme | Ref<Theme> | Current theme |
site | Ref<SiteInfo | null> | Site info (colors, auth mode) |
| Method | Description |
|---|
login(redirect?) | Navigate to Simple Directory login |
logout(redirect?) | Delete auth cookies and redirect |
switchOrganization(orgId, dep?, role?) | Switch active organization |
switchLang(lang) | Change language (triggers page reload) |
keepalive() | Refresh the JWT token |
Keepalive runs automatically every 10 minutes on non-iframe top windows. Changing account, lang, or dark triggers a full page reload to ensure data consistency.
Common Patterns Summary
- Express init:
session.init(directoryUrl) at startup
- Express middleware:
session.middleware() on route groups
- Read session:
reqSession(req) or reqSessionAuthenticated(req) (sync, after middleware)
- Check permission:
assertAccountRole(session, resource.owner, 'admin')
- Super-admin gate:
assertAdminMode(session)
- List filtering: scope queries to
session.account.{type, id, department}
- Default owner:
body.owner ?? session.account
- Vue setup:
createSession({}) as Vue plugin
- Vue access:
useSession() for public pages, useSessionAuthenticated() for protected pages
- Vue permission:
getAccountRole(session.state, owner) for conditional UI