DynamoDB-based session store for express-session, using the AWS SDK for JS v3, offering configurability for cost, performance, and reliability not found in other DynamoDB session stores.
DynamoDB is an excellent choice for session stores because it is a fully managed service that is highly available, durable, and can scale automatically (to nearly unlimited levels) to meet demand. DynamoDB reads will typically return in 1-3 ms if capacity is set correctly and the caller is located in the same region as the Table
.
The package is available on npm as @pwrdrvr/dynamodb-session-store
npm i @pwrdrvr/dynamodb-session-store
import { DynamoDBStore } from '@pwrdrvr/dynamodb-session-store';
The following IAM permissions are required for the DynamoDB Table:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:UpdateItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/dynamodb-session-store-test"
}
]
}
See the examples directory for more examples
import express from 'express';
import session from 'express-session';
import { DynamoDBStore } from '@pwrdrvr/dynamodb-session-store';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const app = express();
const dynamoDBClient = new DynamoDBClient({});
app.use(
session({
store: new DynamoDBStore({
tableName: 'some-table',
dynamoDBClient,
touchAfter: 60 * 60, // 60 minutes in seconds
}),
secret: 'yeah-change-this',
cookie: {
maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days in milliseconds
},
resave: false,
saveUninitialized: false,
}),
);
app.get('/login', (req, res) => {
console.log(`Session ID: ${req.session?.id}`);
req.session.user = 'test';
res.send('Logged in');
});
app.get('/*', (req, res) => {
res.status(200).send('Hello world');
});
app.listen(Number.parseInt(PORT, 10), () => {
console.log(`Example app listening on port ${port}`);
});
The following field types are fully supported by the DynamoDBStore
:
string
number
boolean
object
The following field types are partially supported by the DynamoDBStore
:
Date
- Stored as a string in ISO 8601 format
- Will be returend as a string in ISO 8601 format
- Cannot be automatically converted back into a
Date
object since it is not known which fields were originallyDate
objects vs date strings - Note: connect-dynamodb serializes
Date
objects to strings as well and also does not support automatic conversion back toDate
objects since it serializes using JSON.stringify() and JSON.parse()
After installing the package review the API Documentation for detailed on each configuration option.
- Configurability of
Strongly Consistent Reads
- Off by default as Eventually Consistent Reads are less expensive and more reliable
- Configurability of PoC-level Table creation
- This should only be used during PoCs, else tables will be created in any accounts that developers point at using credentials that have permissions to create tables
- Cost reduction through reducing the TTL write on every read via
.touch()
calls fromexpress-session
- This PR for
express-session
would have made that a configurable option for every session store, but alas, it was rejected in favor of implementing the same thing 15+ times - These TTL writes consumed WCUs based on the size of the entire session, not just the
expires
field - These writes were 10x more expensive than reads for under 1 KB session with ConsistentReads turned off
- These writes were 40x more expensive than reads for 3-4 KB session with consistent reads turned off
- Cost of 1 WCU is 5x that of 1 RCU
- Eventually Consistent Read of 4 KB takes 0.5 RCU
- Write of 1 KB takes 1 WCU, write of 4 KB takes 4 WCU
- This PR for
- Use a Table per-region if the app is deployed in multiple regions
- Use a Table per-environment if the app is deployed in multiple environments (e.g. dev/qa/prod)
- Use a Table unique to the session store - do not try to overload other data into this Table as the scaling and expiration needs will not overlap well
- For applications attached to a VPC (including Lambda's attached to a VPC), use a VPC Endpoint for DynamoDB to avoid the cost, latency, and additional reliability exposure of crossing NAT Gateway to reach DynamoDB
- Use Provisioned Capacity with auto-scaling to avoid throttling and to achieve the lowest cost - On Demand seems nice but it is costly
Disclaimer: perform your own pricing calculation, monitor your costs during and after initial launch, and setup cost alerts to avoid unexpected charges.
- Using Provisioned Capacity with auto-scaling
- Using Eventually Consistent Reads
- 2 KB average session size
- 100k RPM (requests per minute) average load
- 1 million new sessions per month (~0.4 new sessions / second)
- 8 million existing sessions
- 2 million session updates / expirations per month (~0.8 updates / second)
- Storage
- 2 KB * 8 million = 16 GB of storage
- 16 GB * $0.25 / GB / month = $4 / month for storage
- Reads
- 100k RPM / 60 seconds = ~1,700 RPS (requests per second)
- 1 RCU (read capacity unit) per item * 0.5 (eventually consistent reads) = 0.5 RCU per read
- 1,700 RPS * 0.5 RCU per read = 850 RCUs
- 850 RCUs / read * 720 hours / month * $0.00013 / RCU / hour = ~$80 / month for reads
- Writes
- 0.4 new sessions / second + 0.8 updates / second = 1.2 WPS (writes per second)
- 1.2 WPS * 2 WCU (write capacity unit) per item = 2.4 WCUs
- Allocate more WCUs to handle bursts
- 100 WCUs * 720 hours / month * $0.00065 / WCU / hour = ~$50 / month for writes
- Total
- $4 / month for storage
- $80 / month for reads
- $50 / month for writes
- $134 / month total
Source: ./examples/express.ts
- Create DynamoDB Table using AWS Console or any other method
- AWS CLI Example:
aws dynamodb create-table --table-name dynamodb-session-store-test --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST
- Default name is
dynamodb-session-store-test
- Default partition key is
id
- No sort key
- On-demand throughput is sufficient for the example, although not suggested for high volume use
- Time to live can be turned on for a field named
expires
aws dynamodb update-time-to-live --table-name dynamodb-session-store-test --time-to-live-specification "Enabled=true, AttributeName=expires"
- AWS CLI Example:
npm run example:express
- If the table name was changed:
TABLE_NAME=my-table-name npm run example:express
- If the table name was changed:
- Load
http://localhost:3001/login
in a browser - Observe that a cookie is returned and does not change
Source: ./examples/cross-account.ts
This example has the DynamoDB in one account and the express app using an IAM role from another account to access the DynamoDB Table using temporary credentials from an STS AssumeRole call (neatly encapsulated by the AWS SDK for JS v3).
This example is more involved than the others as it requires setting up an IAM role that can be assumed by the app account.
Instructions for Cross-Account DynamoDB Table Example
Source: ./examples/other.ts
- Create DynamoDB Table using AWS Console or any other method
- AWS CLI Example:
aws dynamodb create-table --table-name connect-dynamodb-test --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST
- Default name is
dynamodb-session-store-test
- Default partition key is
id
- No sort key
- On-demand throughput is sufficient for the example, although not suggested for high volume use
- Time to live can be turned on for a field named
expires
aws dynamodb update-time-to-live --table-name connect-dynamodb-test --time-to-live-specification "Enabled=true, AttributeName=expires"
- AWS CLI Example:
npm run example:express
- If the table name was changed:
TABLE_NAME=my-table-name npm run example:other
- If the table name was changed:
- Load
http://localhost:3001/login
in a browser - Observe that a cookie is returned and does not change
Comparison with dynamodb-connect
- Removes dangerous
scan
functionality that can make the DB completely unavailable if accidentally invoked - Uses eventually consistent reads on DynamoDB by default to reduce cost, increase throughput, and improve reliability (configurable)
- Allows skipping
touch
calls that update the TTL on every read, which can be very expensive (configurable) - Serializes the
sess
field to aMap
on DynamoDB for easier querying vsdynamodb-connect
which serializes to aString
- Migration from
dynamodb-connect
is easy, just set theprefix
,hashKey
, andtable
fields to the same values asdynamodb-connect
- theget
function will automatically deserialize the JSONsess
field if found - Examples of how to use the module with
express-session
are provided
{
"id": "123",
"sess": {
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"name": "paul"
},
"expires": 1621968000
}
![image](https://private-user-images.githubusercontent.com/5617868/251259332-fdc9e6b4-4b28-4562-bf51-48b43d5555b1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ3ODA1ODgsIm5iZiI6MTcyNDc4MDI4OCwicGF0aCI6Ii81NjE3ODY4LzI1MTI1OTMzMi1mZGM5ZTZiNC00YjI4LTQ1NjItYmY1MS00OGI0M2Q1NTU1YjEucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDgyNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA4MjdUMTczODA4WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YzBkNjlkMzQ4NDA4NmY0NDc3ZTRmMzMxOGU1NzMwZjFiNzEzOGJhNmIyMDkwNmFiZDEwZjI3ZDNmZmVmMGVhMyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.wIvHxXXF-oHz7H_fVts5YllZGHpjUj9xVrkF7OZ-xyw)
{
"id": "123",
"sess": "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"name\":\"paul\"}",
"expires": 1621968000
}
![image](https://private-user-images.githubusercontent.com/5617868/251259365-7815582a-c12a-49ec-83c0-323d76d441a6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ3ODA1ODgsIm5iZiI6MTcyNDc4MDI4OCwicGF0aCI6Ii81NjE3ODY4LzI1MTI1OTM2NS03ODE1NTgyYS1jMTJhLTQ5ZWMtODNjMC0zMjNkNzZkNDQxYTYucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDgyNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA4MjdUMTczODA4WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YjQ4NGJlZWQyZmI1Y2NiMGVjY2M1MGY0YjU3NWJhYmYwOWQ0ZDQzYjAwYWNkMzc3NWNiMjEwODBiOWRlZjI0MCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.TOHpGu6g6OZXR2yi27R-z6u-KF-2v77Ebyialj5Y5Uw)