Session

The support for sessions is provided by the @adonisjs/session package. The package comes pre-configured with the web starter template. However, installing and configuring it is also relatively straightforward.

npm i @adonisjs/session
node ace configure @adonisjs/session
# CREATE: config/session.ts
# UPDATE: .env { "SESSION_DRIVER = cookie" }
# UPDATE: .adonisrc.json { providers += "@adonisjs/session" }
/**
* Make sure to add the following validation rules to the
* `env.ts` file to validate the environment variables.
*/
export default Env.rules({
// ...existing rules
SESSION_DRIVER: Env.schema.string(),
})
  • Support for multiple drivers. Cookies, File and Redis
  • Allows instantiating session store in read only mode (helpful during websocket requests).
  • Support for session flash messages

Session configuration

You can configure the behavior of the session by tweaking the config/session.ts file. Following is the default config file.

config/session.ts
{
enabled: true,
driver: Env.get('SESSION_DRIVER'),
cookieName: 'adonis-session',
clearWithBrowser: false,
age: '2h',
cookie: {}, // see the cookie driver
file: {}, // see the file driver
redisConnection: 'local', // see the redis driver
}

Session drivers

The session package allows you to choose between one of the available drivers to save the session data.

You can configure the driver inside the config/session.ts file. The driver property, in turn, relies on the SESSION_DRIVER environment variable.

config/session.ts
{
driver: Env.get('SESSION_DRIVER'),
}

The cookie driver uses the HTTP cookies for storing the session data. The session data is encrypted inside the cookie, so you don't have to worry about leaking sensitive information.

The cookie driver also works great even when your application is behind a load balancer since no information is stored on the server.

You can tweak the settings for the cookie driver inside the config/session.ts file.

{
/*
|---------------------------------------------------------------
| Cookies config
|---------------------------------------------------------------
|
| The cookie settings are used to set up the session id cookie
| and also the driver will use the same values.
|
*/
cookie: {
path: '/',
httpOnly: true,
sameSite: false,
},
}

File driver

The file driver stores the session data on the server filesystem. You can configure the storage location by updating the value of the file.location property inside the config/session.ts file.

Running an application behind a load balancer with the file driver requires you to enable sticky sessions on your load balancer.

{
file: {
location: Application.tmp('sessions'),
},
}

Redis

The redis driver is an ideal choice for keeping the session data on the server only and still run your application behind a load balancer.

The redis driver relies on the @adonisjs/redis package. Make sure to configure it first.

The configuration for the redis driver references one of the pre-defined redis connections inside the config/redis.ts file.

config/session.ts
{
driver: 'redis',
redisConnection: 'local',
}

Next, define a connection named local inside the config/redis.ts file.

config/redis.ts
{
connections: {
local: {
host: Env.get('REDIS_HOST'),
port: Env.get('REDIS_PORT'),
password: Env.get('REDIS_PASSWORD', ''),
db: 0,
}
}
}

Read/Write session values

You can interact with sessions by using the ctx.session property.

Route.get('/', async ({ session }) => {
// Read value
const cartTotal = session.get('cart_total')
// Write value
session.put('cart_total', cartTotal + 10)
})

A read-only version of the session is also available inside the Edge templates. You can access it using the session global helper.

<p> Cart total: {{ session.get('cart_total', 0) }} </p>

Following is the list of available methods to work sessions.

get

Read the value for a given key from the session store. Optionally, you can define a default value to return when the actual value is undefined or null.

The following method is available inside the templates as well.

session.get('cart_total')
session.get('cart_total', 0)

put

Write a key-value pair to the session store. The value should be one of the cookie supported data types .

session.put('cart_total', 1900)

all

Read everything from the session store. Will always be an object of a key-value pair.

The following method is available inside the templates as well.

console.log(session.all())

forget

Remove value for a given key from the session store.

// Remove
session.forget('cart_total')
session.get('cart_total') // undefined

increment

Increment the value for a given key. Make sure the original value is always a number. Calling increment on a non-numeric value will result in an exception.

session.increment('page_views')

decrement

Decrement the value for a given key. Make sure the original value is always a number. Calling decrement on a non-numeric value will result in an exception.

session.decrement('score')

clear

Clear the session store to an empty state.

session.clear()

Session id lifecycle

AdonisJS creates an empty session store and assigns it to a unique session id on the first HTTP request, even if the request/response lifecycle doesn't interact with sessions.

Every subsequent request will update the maxAge property of the session id cookie to ensure that it doesn't expire. Also, the session driver is notified about the changes (if any) to update and persist the changes.

sessionId

You can access the value of the session id using the sessionId property.

console.log(session.sessionId)

initiated

Find if the session store has been initiated or not. During an HTTP request, this will always be true.

if (!session.initiated) {
await session.initiate(false)
}

fresh

Find if the session id has been generated during the current HTTP request. The value is true when the session id has been generated for the first time or the session.regenerate method is called.

if (!session.fresh) {
session.regenerate()
}

regenerate

Re-generate the session id and attach existing session data to it. The auth package uses this method to prevent session hijacking attacks .

session.regenerate()

Session flash messages

Flash messages are stored inside the session store and only available for the next HTTP request. You can use them for passing messages between HTTP requests. For example:

Route.get('/', async ({ session, response }) => {
session.flash('message', 'Hello world')
response.redirect('/see-message')
})
Route.get('/see-message', async ({ session }) => {
return session.flashMessages.get('message')
})

flash

The session.flash method adds a key-value pair to the flash messages.

session.flash('errors', {
title: 'Post title is required',
description: 'Post description is required',
})

You can also pass the object directly to the flash method.

session.flash({
errors: {
title: 'Post title is required',
description: 'Post description is required',
},
})

flashAll

The flashAll method adds the request body to the flash messages. This allows you to get the form data inside your templates and pre-fill the user input after validation failure redirect.

session.flashAll()

flashOnly

The session.flashOnly method is similar to the flashAll method. But instead, it allows cherry-picking the fields.

session.flashOnly(['title', 'description'])

flashExcept

The session.flashExcept method is the opposite of the flashOnly method and allows ignoring the fields.

session.flashExcept(['_csrf', 'submit'])

Accessing flash messages

You can access the flash messages set by the previous request using the session.flashMessages property or the flashMessages helper inside the edge templates.

Inside templates
{{-- Get value for a given key --}}
{{ flashMessages.get('errors.title') }}
{{-- With optional default value --}}
{{ flashMessages.get('title', '') }}
{{-- Find if a key exists --}}
{{ flashMessages.has('errors.title') }}
{{-- Get all --}}
{{ flashMessages.all() }}
{{-- Find if store is empty --}}
{{ flashMessages.isEmpty }}
Route.get('/', async ({ session }) => {
// Get value for a given key
response.flashMessages.get('errors.title')
// With optional default value
response.flashMessages.get('title', '')
// Find if a key exists
response.flashMessages.has('errors.title')
// Get all
response.flashMessages.all()
// Find if store is empty
response.flashMessages.isEmpty
})

Other methods/properties

Following is the list of other available methods and properties on the Session class.

initiate

The session.initiate method initiates the session store for the current HTTP request. Optionally, You can initiate the store in readonly mode.

This method is called by AdonisJS automatically, and hence you won't have to call it manually yourself.

await session.initiate(false)
// Readonly store
await session.initiate(true)

fresh

Find if the session id has been generated during the current HTTP request. The value is true when the session id has been generated for the first time or the session.regenerate method is called.

if (!session.fresh) {
session.regenerate()
}

readonly

Find if the store has been initiated in readonly mode or not.

During HTTP requests, the store is NEVER in read-only mode. This flag is reserved for the future to have a read-only session for Websockets connections.

if (!session.readonly) {
session.put('key', 'value')
}

commit

The commit method persists the session driver's changes and updates the maxAge of the session id cookie. The commit method is called automatically by AdonisJS, and you don't have to worry about calling it.

await session.commit()

Creating a custom session driver

The session package exposes the API to add your custom session drivers. Every session driver must adhere to the SessionDriverContract .

interface SessionDriverContract {
read(
sessionId: string
): Promise<Record<string, any> | null> | Record<string, any> | null
write(sessionId: string, values: Record<string, any>): Promise<void> | void
destroy(sessionId: string): Promise<void> | void
touch(sessionId: string): Promise<void> | void
}

read

The read method receives the sessionId and must return the session data or null. The return value should be an object, similar to what was passed to the write method.


write

The write method receives the sessionId and the values object to store. You are free to transform the values object to any other data type you want. For example, The redis driver converts the object to a string using the message builder .


destroy

The destroy method should remove the session id and its associated data from the storage.


touch

The touch method's job is to reset the expiry. This method is only relevant for drivers with in-built expiry. For example, The redis driver updates the ttl property of the redis key.


Extending from outside in

Anytime you are extending the core of the framework. It is better to assume that you do not have access to the application code and its dependencies. In other words, write your extensions as if you are writing a third-party package and use dependency injection to rely on other dependencies.

For demonstration purposes, let's create a session driver to store the session in-memory and begin by making some files & folders.

mkdir providers/SessionDriver
touch providers/SessionDriver/index.ts

The directory structure will look as follows.

providers
└── SessionDriver
└── index.ts

Open the SessionDriver/index.ts file and paste the following contents inside it.

providers/SessionDriver/index.ts
import { SessionDriverContract } from '@ioc:Adonis/Addons/Session'
const SESSIONS: Map<string, Record<string, any>> = new Map()
export class MemoryDriver implements SessionDriverContract {
public async read(sessionId: string) {
return SESSIONS.get(sessionId) || null
}
public async write(sessionId: string, values: Record<string, any>) {
SESSIONS.set(sessionId, values)
}
public async destroy(sessionId: string) {
SESSIONS.delete(sessionId)
}
public async touch() {}
}

Finally, open the providers/AppProvider.ts file and add the custom driver inside the boot method.

import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
public static needsApplication = true
constructor(protected app: ApplicationContract) {}
public async boot() {
const { MemoryDriver } = await import('./SessionDriver')
const Session = this.app.container.use('Adonis/Addons/Session')
Session.extend('memory', () => {
return new MemoryDriver()
})
}
}

Voila! The memory driver is ready now. Just update the SESSION_DRIVER property inside the .env file, and you are good to go.

Driver lifecycle

A new instance of the driver is created for every HTTP request. You can access the HTTP context from the Session.extend method callback arguments. For example:

Session.get('memory', (sessionManager, config, ctx) => {
// incase your driver needs the context
return new Driver(ctx)
})

Injecting dependencies

As mentioned earlier, the extensions should not rely on the application dependencies directly and instead leverage the dependency injection.

For example, if your driver needs access to the Encryption module, it should accept it as a constructor argument vs. importing it directly.

/**
* The following is type-only import and gets removed
* once TypeScript is compiled to JavaScript.
*
* So ideally, you are not relying on any top level
* imports, just using the interface for type hinting.
*/
import type { EncryptionContract } from '@ioc:Adonis/Core/Encryption'
export class MemoryDriver {
constructor(private encryption: EncryptionContract) {}
public async write(sessionId: string, values: { [key: string]: any }) {
this.encryption.encrypt(JSON.stringify(values))
}
}

Finally, you can inject the encryption module during the Session.extend call.

Session.extend('memory', ({ app }) => {
return new MemoryDriver(app.container.use('Adonis/Core/Encryption'))
})

Driver config

You must also inject the configuration for the driver through the constructor. The session.extend method gives you the config saved inside the config/session.ts file.

The configuration for the driver is stored inside a property matching the driver's name. For example:

config/session.ts
{
// The following object is for the memory driver
memory: {}
}
Session.extend('memory', (app, config) => {
/**
* The config is the value of the "memory" property
*/
return new MemoryDriver(config)
})