Version: 1.16.0 | Updated: 2026-03-25
publicActionsis the foundation - Only whitelisted methods can be called remotely- Declarative auth via
static authandstatic actionAuth - Custom
authorize()function for any logic (DB checks, plans, feature flags) - Role-based (OR logic) and permission-based (AND logic) access control
$auth.session— generic session data (user, bot, device, service — dev defines)- Token always sent inside WebSocket (never in URL)
- Auto re-mount when authentication changes
$authavailable on frontend with session data from server
Two ways to handle auth — use either or both:
Global auth for the WebSocket connection. All components share the same $auth.
// Server: create provider
class JWTProvider implements LiveAuthProvider {
readonly name = 'jwt'
async authenticate(credentials: LiveAuthCredentials) {
const decoded = jwt.verify(credentials.token, SECRET)
return new AuthenticatedContext({
id: decoded.sub,
name: decoded.name,
plan: decoded.plan,
roles: decoded.roles,
permissions: decoded.permissions,
})
}
}
// Server: register
liveAuthManager.register(new JWTProvider())
// Client: login
const { authenticate } = useLiveComponents()
await authenticate({ token: jwtToken })Auth inside the component itself. No provider needed.
// Server
export class LiveChat extends LiveComponent<
{ messages: Message[] }, // state: client reads/writes
{ loggedIn: boolean; odId: string } // $private: invisible to client
> {
static publicActions = ['login', 'sendMessage'] as const
async login(payload: { email: string; password: string }) {
const user = await db.findByEmail(payload.email)
if (!user) return { success: false }
this.$private.loggedIn = true
this.$private.odId = user.id
return { success: true, name: user.name }
}
async sendMessage(payload: { text: string }) {
if (!this.$private.loggedIn) throw new Error('Not authenticated')
// ...
}
}
// Client
const chat = Live.use(LiveChat)
const result = await chat.login({ email, password })export class ProtectedChat extends LiveComponent<State> {
static componentName = 'ProtectedChat'
static publicActions = ['sendMessage'] as const
static defaultState = { messages: [] as string[] }
static auth = { required: true }
async sendMessage(payload: { text: string }) {
const userId = this.$auth.session?.id
return { success: true }
}
}export class AdminPanel extends LiveComponent<State> {
static componentName = 'AdminPanel'
static publicActions = ['deleteUser'] as const
static defaultState = { users: [] }
// OR logic: admin OR moderator
static auth = { required: true, roles: ['admin', 'moderator'] }
async deleteUser(payload: { userId: string }) {
console.log(`${this.$auth.session?.id} deleting ${payload.userId}`)
return { success: true }
}
}export class ContentEditor extends LiveComponent<State> {
static componentName = 'ContentEditor'
static publicActions = ['editContent'] as const
static defaultState = { content: '' }
// AND logic: ALL permissions required
static auth = { required: true, permissions: ['content.read', 'content.write'] }
}For any logic beyond roles/permissions — DB lookups, plan checks, feature flags, etc.
export class ProDashboard extends LiveComponent<State> {
static componentName = 'ProDashboard'
static publicActions = ['getData'] as const
static auth = {
required: true,
// Runs AFTER declarative checks (required, roles, permissions)
authorize: async (auth) => {
const plan = await db.getUserPlan(auth.session?.id)
if (plan !== 'pro') {
return { allowed: false, reason: 'Pro plan required' }
}
return true
}
}
}export class ModerationPanel extends LiveComponent<State> {
static componentName = 'ModerationPanel'
static publicActions = ['getReports', 'deleteReport', 'banUser'] as const
static auth = { required: true }
static actionAuth = {
deleteReport: { permissions: ['reports.delete'] },
banUser: { roles: ['admin', 'moderator'] },
}
// Action authorize receives the payload
static actionAuth = {
editProfile: {
authorize: (auth, payload) => auth.session?.id === payload.userId
},
adminDelete: {
roles: ['admin'],
authorize: async (auth, payload) => {
if (payload.itemId === 'protected') {
return { allowed: false, reason: 'Item is protected' }
}
return true
}
},
}
}Auth configs are plain objects — compose and reuse them:
// auth/rules.ts
export const adminOnly = { required: true, roles: ['admin'] }
export const proOnly = {
required: true,
authorize: async (auth) => {
const plan = await db.getUserPlan(auth.session?.id)
return plan === 'pro'
}
}
export const ownerOnly = (field = 'userId') => ({
authorize: (auth, payload) => auth.session?.id === payload?.[field]
})
// Components import and use
export class LiveAdminPanel extends LiveComponent<State> {
static auth = adminOnly
static actionAuth = { delete: ownerOnly('targetUserId') }
}export class MyComponent extends LiveComponent<State> {
async myAction() {
// Session data (shape defined by your provider)
const id = this.$auth.session?.id
const name = this.$auth.session?.name
const plan = this.$auth.session?.plan
// Built-in helpers
this.$auth.authenticated // boolean
this.$auth.hasRole('admin') // boolean
this.$auth.hasAnyRole(['admin', 'mod'])
this.$auth.hasAllRoles(['user', 'verified'])
this.$auth.hasPermission('chat.write')
this.$auth.hasAllPermissions(['users.read', 'users.write'])
this.$auth.hasAnyPermission(['chat.read', 'chat.write'])
return { id }
}
}interface LiveAuthContext {
readonly authenticated: boolean
readonly session?: LiveAuthSession // dev defines the shape
readonly user?: LiveAuthSession // deprecated alias for session
readonly token?: string
readonly authenticatedAt?: number
hasRole(role: string): boolean
hasAnyRole(roles: string[]): boolean
hasAllRoles(roles: string[]): boolean
hasPermission(permission: string): boolean
hasAnyPermission(permissions: string[]): boolean
hasAllPermissions(permissions: string[]): boolean
}
interface LiveAuthSession {
id: string
roles?: string[]
permissions?: string[]
[key: string]: unknown // dev adds any fields
}this.state → client reads AND writes (bidirectional sync)
this.$private → client NEVER sees (server-only)
this.$auth → set by framework, immutable, read-only
Use $private for sensitive flags (loggedIn, internal IDs). Use state for display data. Never trust state for security — the client can send PROPERTY_UPDATE to modify it.
<LiveComponentsProvider
auth={{ token }} // Sent via AUTH message inside WebSocket (never in URL)
autoConnect={true}
>
<App />
</LiveComponentsProvider>function LoginForm() {
const { authenticated, authenticate, $auth } = useLiveComponents()
const handleLogin = async () => {
const success = await authenticate({ token })
// Components with AUTH_DENIED auto re-mount
}
// Session data from the server
console.log($auth.session?.name)
const handleLogout = () => reconnect() // reconnect without token = anonymous
}const panel = Live.use(AdminPanel)
panel.$authenticated // boolean
panel.$auth.authenticated // boolean
panel.$auth.session // { id, name, roles, ... } or null
if (panel.$error?.includes('AUTH_DENIED')) {
return <p>Access denied</p>
}1. Live.use(AdminPanel) → AUTH_DENIED (not logged in)
2. authenticate({ token: 'admin-token' })
3. AdminPanel re-mounts automatically
import type { LiveAuthProvider, LiveAuthCredentials, LiveAuthContext } from '@fluxstack/live'
import { AuthenticatedContext } from '@fluxstack/live'
export class MyAuthProvider implements LiveAuthProvider {
readonly name = 'my-auth'
async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
const token = credentials.token as string
if (!token) return null
const user = await validateToken(token)
if (!user) return null
// Everything here goes to $auth.session
return new AuthenticatedContext({
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
plan: user.plan,
roles: user.roles,
permissions: user.permissions,
}, token)
}
// Optional: custom action authorization
async authorizeAction(context: LiveAuthContext, componentName: string, action: string): Promise<boolean> {
return true
}
// Optional: custom room authorization
async authorizeRoom(context: LiveAuthContext, roomId: string): Promise<boolean> {
if (roomId.startsWith('vip-') && !context.hasRole('premium')) return false
return true
}
}The session is generic — not limited to users:
// Bot provider
return new AuthenticatedContext({
id: 'bot-1',
type: 'bot',
botName: 'NotifyBot',
allowedChannels: ['general', 'alerts'],
roles: ['bot'],
})
// Device/IoT provider
return new AuthenticatedContext({
id: 'sensor-42',
type: 'device',
model: 'TempSensor-v3',
location: { lat: -23.5, lng: -46.6 },
roles: ['device'],
})// app/server/index.ts
import { liveAuthManager } from '@core/server/live'
liveAuthManager.register(new MyAuthProvider())- Blocklist - Internal methods (destroy, setState, emit) always blocked
- Private methods -
_xor#xblocked - publicActions - Must be in whitelist (mandatory)
- actionAuth - Declarative roles/permissions + custom
authorize() - Method exists - Must exist on instance
- Object.prototype - toString, valueOf blocked
1. static auth.required? → authenticated?
2. static auth.roles? → hasAnyRole() (OR)
3. static auth.permissions? → hasAllPermissions() (AND)
4. static auth.authorize? → custom function
All pass → mount allowed
1. actionAuth[action].roles? → hasAnyRole() (OR)
2. actionAuth[action].permissions? → hasAllPermissions() (AND)
3. actionAuth[action].authorize? → custom function(auth, payload)
4. provider.authorizeAction? → if provider implements
All pass → action allowed
interface LiveComponentAuth {
required?: boolean
roles?: string[]
permissions?: string[]
authorize?: (auth: LiveAuthContext) => boolean | { allowed: boolean; reason?: string } | Promise<...>
}
interface LiveActionAuth {
roles?: string[]
permissions?: string[]
authorize?: (auth: LiveAuthContext, payload: unknown) => boolean | { allowed: boolean; reason?: string } | Promise<...>
}
type LiveActionAuthMap = Record<string, LiveActionAuth>ALWAYS:
- Define
static publicActions(MANDATORY — without it, ALL actions are denied) - Define
static authfor protected components - Use
$privatefor sensitive data, neverstate - Register auth providers before server starts
- Handle
AUTH_DENIEDerrors in client UI
NEVER:
- Store auth flags in
state(client can modify via PROPERTY_UPDATE) - Trust client-side auth checks alone
- Expose tokens in error messages
- Live Components - Base component documentation
- Live Rooms - Room-based communication