Specialized skill for designing AWS DynamoDB single-table schemas with optimized access patterns. Use when modeling data, designing table structure, or optimizing DynamoDB queries for production applications.
Designs optimized DynamoDB single-table schemas with proper key structures, GSIs, and access patterns. Use when modeling data for NoSQL databases or optimizing query performance for production applications.
/plugin marketplace add swapkats/robin/plugin install robin@swapkats-robinThis skill inherits all available tools. When active, it can use any tool Claude has access to.
You are an expert in designing production-ready DynamoDB single-table schemas optimized for performance, cost, and scalability.
ONE table per application. Always. No exceptions.
Partition Key (PK): STRING
Sort Key (SK): STRING
Always use generic names PK and SK. This allows flexibility for any entity type.
PK STRING (Partition Key)
SK STRING (Sort Key)
EntityType STRING (e.g., "User", "Post", "Comment")
GSI1PK STRING (GSI #1 Partition Key)
GSI1SK STRING (GSI #1 Sort Key)
GSI2PK STRING (GSI #2 Partition Key) [optional]
GSI2SK STRING (GSI #2 Sort Key) [optional]
...entity-specific attributes...
CreatedAt STRING (ISO 8601 timestamp)
UpdatedAt STRING (ISO 8601 timestamp)
Access Patterns:
Design:
User Item:
PK: USER#<userId>
SK: PROFILE
EntityType: User
GSI1PK: USER#<email>
GSI1SK: USER#<email>
Email: user@example.com
Name: John Doe
CreatedAt: 2024-01-01T00:00:00Z
UpdatedAt: 2024-01-01T00:00:00Z
Queries:
// Get user by ID
const user = await docClient.get({
TableName: TABLE_NAME,
Key: {
PK: `USER#${userId}`,
SK: 'PROFILE',
},
});
// Get user by email (using GSI1)
const user = await docClient.query({
TableName: TABLE_NAME,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :email',
ExpressionAttributeValues: {
':email': `USER#${email}`,
},
});
Example: User has many Posts
Access Patterns:
Design:
User Item:
PK: USER#<userId>
SK: PROFILE
EntityType: User
...
Post Item:
PK: USER#<userId>
SK: POST#<timestamp>#<postId>
EntityType: Post
GSI1PK: POST#<postId>
GSI1SK: POST#<postId>
GSI2PK: ALL_POSTS
GSI2SK: POST#<timestamp>
PostId: <postId>
Title: Post title
Content: Post content
CreatedAt: 2024-01-01T00:00:00Z
Queries:
// Get all posts by user (sorted by timestamp, newest first)
const posts = await docClient.query({
TableName: TABLE_NAME,
KeyConditionExpression: 'PK = :userId AND begins_with(SK, :prefix)',
ExpressionAttributeValues: {
':userId': `USER#${userId}`,
':prefix': 'POST#',
},
ScanIndexForward: false, // Descending order
});
// Get specific post by ID (using GSI1)
const post = await docClient.query({
TableName: TABLE_NAME,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :postId',
ExpressionAttributeValues: {
':postId': `POST#${postId}`,
},
});
// Get recent posts from all users (using GSI2)
const posts = await docClient.query({
TableName: TABLE_NAME,
IndexName: 'GSI2',
KeyConditionExpression: 'GSI2PK = :allPosts',
ExpressionAttributeValues: {
':allPosts': 'ALL_POSTS',
},
ScanIndexForward: false,
Limit: 20,
});
Example: Users can like many Posts, Posts can be liked by many Users
Access Patterns:
Design:
Like Item (User's perspective):
PK: USER#<userId>
SK: LIKE#POST#<postId>
EntityType: Like
GSI1PK: POST#<postId>
GSI1SK: LIKE#USER#<userId>
PostId: <postId>
UserId: <userId>
CreatedAt: 2024-01-01T00:00:00Z
Queries:
// Get all posts liked by user
const likes = await docClient.query({
TableName: TABLE_NAME,
KeyConditionExpression: 'PK = :userId AND begins_with(SK, :prefix)',
ExpressionAttributeValues: {
':userId': `USER#${userId}`,
':prefix': 'LIKE#POST#',
},
});
// Get all users who liked a post (using GSI1)
const likes = await docClient.query({
TableName: TABLE_NAME,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :postId AND begins_with(GSI1SK, :prefix)',
ExpressionAttributeValues: {
':postId': `POST#${postId}`,
':prefix': 'LIKE#USER#',
},
});
// Check if user liked specific post
const like = await docClient.get({
TableName: TABLE_NAME,
Key: {
PK: `USER#${userId}`,
SK: `LIKE#POST#${postId}`,
},
});
Example: User > Organization > Team > Member
Design:
Organization:
PK: ORG#<orgId>
SK: METADATA
EntityType: Organization
Name: Acme Corp
Team:
PK: ORG#<orgId>
SK: TEAM#<teamId>
EntityType: Team
GSI1PK: TEAM#<teamId>
GSI1SK: TEAM#<teamId>
TeamId: <teamId>
Name: Engineering
Member:
PK: ORG#<orgId>
SK: MEMBER#<userId>
EntityType: Member
GSI1PK: USER#<userId>
GSI1SK: MEMBER#ORG#<orgId>
UserId: <userId>
Role: Admin
Queries:
// Get organization with all teams
const result = await docClient.query({
TableName: TABLE_NAME,
KeyConditionExpression: 'PK = :orgId',
ExpressionAttributeValues: {
':orgId': `ORG#${orgId}`,
},
});
// Get all organizations a user is member of (using GSI1)
const memberships = await docClient.query({
TableName: TABLE_NAME,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :userId AND begins_with(GSI1SK, :prefix)',
ExpressionAttributeValues: {
':userId': `USER#${userId}`,
':prefix': 'MEMBER#ORG#',
},
});
KEYS_ONLY or INCLUDE projectionsGSI1 (Common reverse lookups):
GSI1PK → GSI1SK
GSI2 (Global queries):
GSI2PK → GSI2SK
Example GSI Setup:
const tableDefinition = {
TableName: TABLE_NAME,
KeySchema: [
{ AttributeName: 'PK', KeyType: 'HASH' },
{ AttributeName: 'SK', KeyType: 'RANGE' },
],
AttributeDefinitions: [
{ AttributeName: 'PK', AttributeType: 'S' },
{ AttributeName: 'SK', AttributeType: 'S' },
{ AttributeName: 'GSI1PK', AttributeType: 'S' },
{ AttributeName: 'GSI1SK', AttributeType: 'S' },
{ AttributeName: 'GSI2PK', AttributeType: 'S' },
{ AttributeName: 'GSI2SK', AttributeType: 'S' },
],
GlobalSecondaryIndexes: [
{
IndexName: 'GSI1',
KeySchema: [
{ AttributeName: 'GSI1PK', KeyType: 'HASH' },
{ AttributeName: 'GSI1SK', KeyType: 'RANGE' },
],
Projection: { ProjectionType: 'ALL' },
},
{
IndexName: 'GSI2',
KeySchema: [
{ AttributeName: 'GSI2PK', KeyType: 'HASH' },
{ AttributeName: 'GSI2SK', KeyType: 'RANGE' },
],
Projection: { ProjectionType: 'ALL' },
},
],
BillingMode: 'PAY_PER_REQUEST',
};
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
await docClient.send(new PutCommand({
TableName: TABLE_NAME,
Item: {
PK: `USER#${userId}`,
SK: 'PROFILE',
EntityType: 'User',
Email: email,
Name: name,
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString(),
},
}));
import { GetCommand } from '@aws-sdk/lib-dynamodb';
const { Item } = await docClient.send(new GetCommand({
TableName: TABLE_NAME,
Key: {
PK: `USER#${userId}`,
SK: 'PROFILE',
},
}));
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
await docClient.send(new UpdateCommand({
TableName: TABLE_NAME,
Key: {
PK: `USER#${userId}`,
SK: 'PROFILE',
},
UpdateExpression: 'SET #name = :name, UpdatedAt = :updatedAt',
ExpressionAttributeNames: {
'#name': 'Name',
},
ExpressionAttributeValues: {
':name': newName,
':updatedAt': new Date().toISOString(),
},
}));
import { DeleteCommand } from '@aws-sdk/lib-dynamodb';
await docClient.send(new DeleteCommand({
TableName: TABLE_NAME,
Key: {
PK: `USER#${userId}`,
SK: 'PROFILE',
},
}));
import { QueryCommand } from '@aws-sdk/lib-dynamodb';
const { Items } = await docClient.send(new QueryCommand({
TableName: TABLE_NAME,
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
':sk': 'POST#',
},
ScanIndexForward: false,
Limit: 20,
}));
import { BatchGetCommand } from '@aws-sdk/lib-dynamodb';
const { Responses } = await docClient.send(new BatchGetCommand({
RequestItems: {
[TABLE_NAME]: {
Keys: [
{ PK: `USER#${userId1}`, SK: 'PROFILE' },
{ PK: `USER#${userId2}`, SK: 'PROFILE' },
{ PK: `USER#${userId3}`, SK: 'PROFILE' },
],
},
},
}));
import { BatchWriteCommand } from '@aws-sdk/lib-dynamodb';
await docClient.send(new BatchWriteCommand({
RequestItems: {
[TABLE_NAME]: [
{
PutRequest: {
Item: {
PK: `USER#${userId1}`,
SK: 'PROFILE',
EntityType: 'User',
// ...
},
},
},
{
PutRequest: {
Item: {
PK: `USER#${userId2}`,
SK: 'PROFILE',
EntityType: 'User',
// ...
},
},
},
],
},
}));
import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
await docClient.send(new TransactWriteCommand({
TransactItems: [
{
Put: {
TableName: TABLE_NAME,
Item: {
PK: `USER#${userId}`,
SK: `LIKE#POST#${postId}`,
EntityType: 'Like',
// ...
},
},
},
{
Update: {
TableName: TABLE_NAME,
Key: {
PK: `USER#${postAuthorId}`,
SK: `POST#${timestamp}#${postId}`,
},
UpdateExpression: 'SET LikeCount = LikeCount + :inc',
ExpressionAttributeValues: {
':inc': 1,
},
},
},
],
}));
// lib/db/types.ts
export interface BaseEntity {
PK: string;
SK: string;
EntityType: string;
CreatedAt: string;
UpdatedAt: string;
}
export interface User extends BaseEntity {
EntityType: 'User';
Email: string;
Name: string;
GSI1PK: string; // USER#<email>
GSI1SK: string; // USER#<email>
}
export interface Post extends BaseEntity {
EntityType: 'Post';
PostId: string;
Title: string;
Content: string;
GSI1PK: string; // POST#<postId>
GSI1SK: string; // POST#<postId>
GSI2PK: string; // ALL_POSTS
GSI2SK: string; // POST#<timestamp>
}
export type Entity = User | Post;
// lib/db/helpers.ts
export function createUserKey(userId: string) {
return {
PK: `USER#${userId}`,
SK: 'PROFILE',
};
}
export function createPostKey(userId: string, timestamp: number, postId: string) {
return {
PK: `USER#${userId}`,
SK: `POST#${timestamp}#${postId}`,
};
}
export function parsePostId(sk: string): string {
const [, , postId] = sk.split('#');
return postId;
}
When designing a new table:
List all access patterns first
Design primary key to satisfy most common pattern
POST#<timestamp>)Add GSI1 for reverse lookups
Add GSI2 for global queries (if needed)
Test query patterns
You design single-table schemas that are fast, cost-effective, and scale infinitely. Period.
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.