Member Journey
Build a complete membership experience with Memberstack, from letting people sign up to managing their accounts. We'll walk through everything step-by-step, covering signup, login, profiles, Member JSON, Data Tables, and protected content.
This guide targets developers using the @memberstack/dom package (or Webflow via window.$memberstackDom). If you are building with Data Attributes (Webflow/WordPress data-attribute integration), use the Data Attribute-specific documentation instead.
Note: The DOM SDK exposes programmatic methods; there are no data-attributes covered here.
Signup Options
Memberstack provides multiple ways for users to sign up: email/password registration, passwordless signup, and social provider authentication. For complete details on implementing each authentication method, see the Core Authentication guide.
Here's an example of implementing email/password signup with custom fields:
<!-- Add this HTML to your signup page -->
<form id="signupForm">
<!-- Basic required fields -->
<input type="email" id="email" placeholder="Email" required />
<input type="password" id="password" placeholder="Password" required />
<!-- Custom fields to collect extra information -->
<input type="text" id="firstName" placeholder="First Name" required />
<input type="text" id="lastName" placeholder="Last Name" required />
<!-- Simple dropdown for user type -->
<select id="userType" required>
<option value="">I am a...</option>
<option value="designer">Designer</option>
<option value="developer">Developer</option>
<option value="other">Other</option>
</select>
<button type="submit">Create Account</button>
<div id="errorMessage" style="color: red; display: none;"></div>
</form>
// Add this JavaScript to handle the signup
document.getElementById('signupForm').addEventListener('submit', async (event) => {
event.preventDefault();
// Hide any previous error messages
const errorDiv = document.getElementById('errorMessage');
errorDiv.style.display = 'none';
// Get all the form values
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const firstName = document.getElementById('firstName').value;
const lastName = document.getElementById('lastName').value;
const userType = document.getElementById('userType').value;
try {
// Try to create the new account
const member = await memberstack.signupMemberEmailPassword({
email: email,
password: password,
// Store additional information in customFields
customFields: {
firstName: firstName,
lastName: lastName,
userType: userType
}
});
// If successful, send them to the dashboard
window.location.href = '/dashboard';
} catch (error) {
// If something goes wrong, show the error message
errorDiv.textContent = error.message || 'Something went wrong. Please try again.';
errorDiv.style.display = 'block';
}
});For more authentication information, check the Core Authentication guide.
- Email/password registration
- Passwordless signup
- Social providers (Google, GitHub, etc.)
- All methods support custom fields for additional member data
Login Options
Memberstack supports multiple authentication methods to give your users flexibility in how they log in. Each method can be implemented independently or combined to provide multiple login options.
Email/Password Login, Traditional authentication with email and password credentials.
Passwordless Login, Secure authentication via a one-time code sent via email, no password required.
Social Provider Login, Authentication through social platforms like Google, GitHub, and others.
For detailed instructions on implementing each login method, visit the Core Authentication guide.
Here's an example implementing email/password and passwordless login:
// Add this HTML to your login page
<div class="login-options">
<!-- Email/Password Login Form -->
<form id="loginForm">
<h3>Log in with Email</h3>
<input type="email" id="loginEmail" placeholder="Email" required />
<input type="password" id="loginPassword" placeholder="Password" required />
<button type="submit">Log In</button>
<div id="loginError" style="color: red; display: none;"></div>
</form>
<!-- Passwordless Login Option -->
<div class="passwordless-section">
<h3>Login without Password</h3>
<form id="passwordlessForm">
<input type="email" id="passwordlessEmail" placeholder="Email" required />
<button type="submit">Send Login Token</button>
<div id="passwordlessMessage"></div>
</form>
</div>
</div>
// Add this JavaScript to handle both login methods
// 1. Email/Password Login
document.getElementById('loginForm').addEventListener('submit', async (event) => {
event.preventDefault();
const errorDiv = document.getElementById('loginError');
errorDiv.style.display = 'none';
try {
const member = await memberstack.loginMemberEmailPassword({
email: document.getElementById('loginEmail').value,
password: document.getElementById('loginPassword').value
});
window.location.href = '/dashboard';
} catch (error) {
errorDiv.textContent = error.message || 'Login failed. Please check your email and password.';
errorDiv.style.display = 'block';
}
});
// 2. Passwordless Login
document.getElementById('passwordlessForm').addEventListener('submit', async (event) => {
event.preventDefault();
const messageDiv = document.getElementById('passwordlessMessage');
const email = document.getElementById('passwordlessEmail').value;
try {
await memberstack.sendMemberLoginPasswordlessEmail({
email: email
});
messageDiv.textContent = 'Check your email for a login token!';
messageDiv.style.color = 'green';
} catch (error) {
messageDiv.textContent = error.message || 'Could not send login token. Please try again.';
messageDiv.style.color = 'red';
}
});- Consider offering both login methods to give users choice
- Add clear instructions for passwordless login
- Make error messages helpful and friendly
- Add a loading state while authentication happens
Profile Management
Allow members to manage their profile information using Memberstack's custom fields. Note that while Memberstack can store URLs to files and images, you'll need to use a separate file storage service (like AWS S3, Cloudinary, etc.) to actually store the files themselves.
Memberstack doesn't handle file storage directly. For profile pictures or other files:
- Upload files to your chosen storage service
- Store the resulting URL in a Memberstack custom field
- Use the stored URL to display the image or file in your UI
Profile Image
The member's profile image is the one exception to the note above: Memberstack hosts it for you. Use updateMemberProfileImage with a File to upload (it resolves to { data: { profileImage } } with the hosted URL), or pass null to remove it. To remove it explicitly you can also call the dedicated deleteMemberProfileImage() method (no arguments), which resolves to { data: { profileImage: null } }. (Other files still belong in your own storage.)
// Upload or replace the member's profile image (must be logged in).
// Memberstack stores the image and returns its hosted URL.
async function uploadProfileImage(fileInput) {
const file = fileInput.files[0];
if (!file) return;
const { data } = await memberstack.updateMemberProfileImage({ profileImage: file });
console.log("New profile image URL:", data.profileImage);
}
// Remove the profile image, pass null.
async function removeProfileImage() {
const { data } = await memberstack.updateMemberProfileImage({ profileImage: null });
console.log("Removed:", data.profileImage === null); // true
}
// Or use the dedicated DELETE method (equivalent to passing null above).
// It also resolves to { data: { profileImage: null } }.
async function deleteProfileImage() {
const { data } = await memberstack.deleteMemberProfileImage();
console.log("Removed:", data.profileImage === null); // true
}Email Verification
Trigger a verification email for the logged-in member with sendMemberVerificationEmail() (it takes no arguments and resolves to { data: { success: true } }). The member's verified state is available as member.verified on the member object.
// Send a verification email to the current member (must be logged in).
// This method takes no arguments.
async function sendVerification() {
try {
const { data } = await memberstack.sendMemberVerificationEmail();
if (data.success) {
console.log("Verification email sent, check your inbox.");
}
} catch (error) {
// "login-required" if no member is logged in
console.error(error.message);
}
}
// Check verification status from the member object:
const { data: member } = await memberstack.getCurrentMember();
const isVerified = member?.verified === true;Here's an example of managing profile information:
<!-- Add this HTML -->
<form id="passwordForm">
<div>
<input
type="password"
id="currentPassword"
placeholder="Current Password"
required
/>
</div>
<div>
<input
type="password"
id="newPassword"
placeholder="New Password"
required
/>
<div id="passwordStrength"></div>
</div>
<button type="submit">Update Password</button>
<div id="message"></div>
</form>
// Add this JavaScript
document.getElementById('passwordForm').addEventListener('submit', async (event) => {
event.preventDefault();
const messageDiv = document.getElementById('message');
try {
await memberstack.updateMemberAuth({
oldPassword: document.getElementById('currentPassword').value,
newPassword: document.getElementById('newPassword').value
});
messageDiv.textContent = 'Password updated!';
messageDiv.style.color = 'green';
document.getElementById('passwordForm').reset();
} catch (error) {
messageDiv.textContent = error.message || 'Update failed. Please try again.';
messageDiv.style.color = 'red';
}
});
// Simple password strength check
document.getElementById('newPassword').addEventListener('input', (event) => {
const password = event.target.value;
const strengthDiv = document.getElementById('passwordStrength');
if (password.length < 8) {
strengthDiv.textContent = 'Too short';
strengthDiv.style.color = 'red';
} else if (/[a-zA-Z]/.test(password) && /[0-9]/.test(password)) {
strengthDiv.textContent = 'Strong';
strengthDiv.style.color = 'green';
} else {
strengthDiv.textContent = 'Add numbers and letters';
strengthDiv.style.color = 'orange';
}
});- All custom fields are stored as strings
- Convert strings to other types as needed (e.g., parseInt for numbers)
- For objects or arrays, use JSON.parse() on the stored string
- Access custom fields through the member object
- Updates are immediately available across your site
String Conversion Examples
// Reading a number from a custom field
const age = parseInt(member.customFields.age);
// Reading a JSON object from a custom field
const preferences = JSON.parse(member.customFields.preferences);
// Storing a number in a custom field
await memberstack.updateMember({
customFields: {
age: age.toString() // Convert to string before storing
}
});
// Storing an object in a custom field
await memberstack.updateMember({
customFields: {
preferences: JSON.stringify(preferences) // Convert to string before storing
}
});Member JSON
Member JSON is a flexible storage system that allows you to associate custom data objects with each member in your app. Unlike custom fields which are predefined in your Memberstack dashboard (Members → Custom Fields), Member JSON allows you to store any valid JSON object without prior configuration.
- User preferences (theme, language, notifications)
- App-specific settings and configurations
- Custom metadata and tracking information
- Application state that persists across sessions
- Any structured data that doesn't fit into standard member fields
Getting Member JSON Data
Use getMemberJSON() to retrieve the stored JSON data for the current member.
// Get the current member's JSON data
const { data } = await memberstack.getMemberJSON();
if (data) {
console.log('User theme:', data.preferences?.theme);
console.log('Dashboard layout:', data.settings?.dashboardLayout);
console.log('Onboarding completed:', data.metadata?.onboardingCompleted);
} else {
console.log('No JSON data found for this member');
}Updating Member JSON Data
Use updateMemberJSON() to store new JSON data. Note that this method completely replaces the existing JSON data.
// Complete replacement of JSON data
await memberstack.updateMemberJSON({
json: {
preferences: {
theme: 'dark',
language: 'en',
notifications: true
},
settings: {
dashboardLayout: 'grid',
itemsPerPage: 25
}
}
});
// To merge with existing data, first fetch current data
const { data: current } = await memberstack.getMemberJSON();
const updatedJson = {
...current,
preferences: {
...current?.preferences,
theme: 'light' // Only update the theme
}
};
await memberstack.updateMemberJSON({ json: updatedJson });updateMemberJSON completely replaces the existing JSON data - it does not merge with existing data. To preserve existing data while updating specific fields, first fetch the current JSON, modify it, then update.
Common Use Cases
User Preferences Storage, Store UI preferences that persist across sessions:
// Save user preferences
await memberstack.updateMemberJSON({
json: {
ui: {
theme: 'dark',
sidebarCollapsed: true,
language: 'en',
timezone: 'America/New_York'
},
notifications: {
email: true,
push: false,
marketing: false
}
}
});
// Load preferences on app start
const { data } = await memberstack.getMemberJSON();
const preferences = data?.ui;
if (preferences) {
applyTheme(preferences.theme);
setSidebarState(preferences.sidebarCollapsed);
setLanguage(preferences.language);
}Progress Tracking and Onboarding, Store user progress and onboarding state:
await memberstack.updateMemberJSON({
json: {
onboarding: {
completed: false,
currentStep: 3,
completedSteps: [1, 2],
skippedSteps: [],
startedAt: '2024-01-15T10:30:00.000Z'
},
progress: {
profileCompletion: 0.75,
coursesCompleted: 5,
badgesEarned: ['first_login', 'profile_complete'],
lastActivity: new Date().toISOString()
}
}
});Feature Flags and A/B Testing, Track feature access and testing participation:
const { data } = await memberstack.getMemberJSON();
const currentData = data || {};
await memberstack.updateMemberJSON({
json: {
...currentData,
experiments: {
...currentData.experiments,
new_checkout_flow: {
variant: 'B',
enrolledAt: new Date().toISOString()
}
},
featureAccess: {
...currentData.featureAccess,
beta_dashboard: true,
advanced_analytics: false
}
}
});1. Structure Your Data, Organize your JSON with clear, consistent naming and logical grouping.
2. Handle Missing Data Gracefully, Always check for data existence and provide defaults: const theme = preferences?.theme || 'light'
3. Merge Don't Replace, Preserve existing data when updating specific fields by fetching current data first.
4. Keep It Lean, Avoid storing large amounts of data or frequently changing information.
Integration with React
Use @memberstack/dom directly in React components. Note: @memberstack/react is deprecated.
import { useEffect, useState } from 'react';
import memberstackDOM from '@memberstack/dom';
// NOTE: @memberstack/react is DEPRECATED. Use @memberstack/dom directly.
function UserPreferences({ memberstack }) {
const [preferences, setPreferences] = useState(null);
useEffect(() => {
async function loadPreferences() {
const { data } = await memberstack.getMemberJSON();
setPreferences(data?.preferences || {});
}
loadPreferences();
}, [memberstack]);
const updateTheme = async (theme: string) => {
// CRITICAL: Always fetch, merge, then update
const { data } = await memberstack.getMemberJSON();
const currentJson = data || {};
await memberstack.updateMemberJSON({
json: {
...currentJson,
preferences: {
...currentJson.preferences,
theme
}
}
});
setPreferences({ ...currentJson.preferences, theme });
};
return (
<div>
<p>Current theme: {preferences?.theme || 'default'}</p>
<button onClick={() => updateTheme('dark')}>Dark Theme</button>
<button onClick={() => updateTheme('light')}>Light Theme</button>
</div>
);
}- Member JSON is tied to the authenticated member and cannot be accessed by other members
- Data should not contain sensitive information like passwords or API keys
- Keep objects reasonably sized for performance
- Subject to standard API rate limits (200 requests per 30 seconds per IP)
- Supports all standard JSON data types but not JavaScript-specific types
- Both methods require an authenticated member session
Data Tables
Data Tables provide a powerful system for storing and managing structured data with advanced querying capabilities, relationships, and access control. Unlike Member JSON which stores unstructured data per member, Data Tables are designed for structured, relational data that can be shared across your application.
- Content management systems (articles, posts, comments)
- E-commerce catalogs (products, categories, reviews)
- Directory systems (listings, profiles, contacts)
- Project management tools (tasks, projects, teams)
- Custom data structures with complex relationships
Global: 200 requests per 30 seconds per IP
Reads: 25 requests per second per IP
Creates: 10 requests per minute per IP
Writes: 30 requests per minute per IP
Reads apply to: GET /v1/data-tables, GET /v1/data-tables/:tableKey, POST /v1/data-records/query, GET /v1/data-records
If DISABLE_DATA_TABLES is truthy on the server, all routes return 503 with message: "Data table feature is temporarilly offline."
Table Management
First, you'll need to understand what tables are available and their structure.
List All Tables, Get all available tables and their schemas:
// Get all available data tables
const { data } = await memberstack.getDataTables();
data.tables.forEach((table) => {
console.log(`Table: ${table.key} (${table.name})`);
console.log(`Records: ${table.recordCount}`);
console.log(`Fields: ${table.fields.length}`);
// List all fields
table.fields.forEach((field) => {
console.log(` - ${field.key}: ${field.type} ${field.required ? '(required)' : ''}`);
});
});Get Single Table, Retrieve detailed information about a specific table:
// Get detailed table information
const { data } = await memberstack.getDataTable({
table: 'articles'
});
console.log('Table name:', data.name);
console.log('Record count:', data.recordCount);
console.log('Access rules:', {
create: data.createRule,
read: data.readRule,
update: data.updateRule,
delete: data.deleteRule
});Record Operations
Perform CRUD operations on individual records within your tables.
Create Records, Add new records to your tables:
// Create a new article record
const { data } = await memberstack.createDataRecord({
table: 'articles',
data: {
title: 'Getting Started with Data Tables',
content: 'Data Tables provide powerful structured data storage...',
published: true,
category: 'tutorial',
tags: ['beginners', 'data-tables', 'guide']
}
});
console.log('Created record:', data.id);
console.log('Created by:', data.createdByMemberId);
console.log('I own this record:', data.activeMemberOwnsIt);Get Records, Retrieve individual records by ID:
// Get a specific record
const { data } = await memberstack.getDataRecord({
table: 'articles',
recordId: 'rec_abc123'
});
console.log('Article title:', data.data.title);
console.log('Published:', data.data.published);
console.log('Created:', data.createdAt);List Records, Use getDataRecords for simple paginated listing of a table. It accepts { table, limit?, after?, sortBy?, sortDirection?, createdAfter?, createdBefore? } and resolves to { data: { records, pagination } }. Reach for queryDataRecords instead when you need field-level filters, compound conditions, or includes.
// Simple paginated listing of a table's records.
// Use getDataRecords for plain listing/pagination by created date;
// use queryDataRecords when you need field-level filters or includes.
const { data } = await memberstack.getDataRecords({
table: 'articles',
limit: 20, // 1-100 records per page
sortBy: 'createdAt',
sortDirection: 'desc',
// Optional date-range filters:
createdAfter: '2024-01-01T00:00:00.000Z',
// createdBefore: '2024-12-31T23:59:59.999Z',
// after: cursorFromPreviousResponse, // internalOrder value
});
console.log('Records:', data.records.length);
console.log('Has more:', data.pagination.hasMore);
console.log('Next cursor:', data.pagination.endCursor);Update Records, Modify existing records with partial updates:
// Update specific fields
await memberstack.updateDataRecord({
recordId: 'rec_abc123',
data: {
title: 'Updated: Getting Started with Data Tables',
published: true,
updatedAt: new Date().toISOString()
}
});
console.log('Record updated successfully');Delete Records, Remove records from your tables:
// Delete a record
await memberstack.deleteDataRecord({
recordId: 'rec_abc123'
});
console.log('Record deleted successfully');getDataRecord requires the table parameter along with recordId, while updateDataRecord and deleteDataRecord only need the recordId.
Advanced Querying
Use powerful query capabilities to filter, sort, and paginate through your data.
Basic Queries, Filter and sort records with simple conditions:
// Query published articles, newest first
const { data } = await memberstack.queryDataRecords({
table: 'articles',
query: {
where: {
published: { equals: true }
},
orderBy: { createdAt: 'desc' },
take: 20
}
});
console.log('Found', data.records.length, 'published articles');
data.records.forEach((record) => {
console.log(`- ${record.data.title} (${record.createdAt})`);
});Complex Filtering, Use advanced operators and compound conditions:
// Complex query with multiple conditions
const { data } = await memberstack.queryDataRecords({
table: 'articles',
query: {
where: {
AND: [
{ published: { equals: true } },
{
OR: [
{ category: { equals: 'tutorial' } },
{ category: { equals: 'guide' } }
]
},
{ title: { contains: 'Data Tables' } },
{ createdAt: { gt: '2024-01-01T00:00:00.000Z' } }
]
},
orderBy: { createdAt: 'desc' },
take: 10
}
});
console.log('Complex query results:', data.records.length);Pagination, Handle large datasets with cursor-based pagination:
// First page
let { data } = await memberstack.queryDataRecords({
table: 'articles',
query: {
where: { published: { equals: true } },
orderBy: { createdAt: 'desc' },
take: 20
}
});
console.log('First page:', data.records.length, 'records');
console.log('Has more:', data.pagination.hasMore);
// Next page using cursor
if (data.pagination?.hasMore && data.pagination.endCursor) {
const nextPage = await memberstack.queryDataRecords({
table: 'articles',
query: {
where: { published: { equals: true } },
orderBy: { createdAt: 'desc' },
take: 20,
after: String(data.pagination.endCursor)
}
});
console.log('Next page:', nextPage.data.records.length, 'records');
}Count Queries, Get total counts without retrieving all records:
// Get total count
const { data } = await memberstack.queryDataRecords({
table: 'articles',
query: {
where: { published: { equals: true } },
_count: true
}
});
console.log('Total published articles:', data._count);Relationships and Includes
Work with related data across tables using includes and relationship fields.
Simple Relationships, Include related records in your queries:
// Include author information with articles
const { data } = await memberstack.queryDataRecords({
table: 'articles',
query: {
where: { published: { equals: true } },
include: {
author: true, // REFERENCE field
postedBy: true // MEMBER_REFERENCE field
},
orderBy: { createdAt: 'desc' },
take: 10
}
});
data.records.forEach((article) => {
console.log(`Article: ${article.data.title}`);
console.log(`Author: ${article.data.author?.data.name}`);
console.log(`Posted by member: ${article.data.postedBy?.email}`);
});Relationship Counts, Get counts of related records without loading them:
// Include relationship counts
const { data } = await memberstack.queryDataRecords({
table: 'articles',
query: {
where: { published: { equals: true } },
include: {
author: true,
_count: {
select: {
comments: true,
likes: true,
tags: true
}
}
},
take: 10
}
});
data.records.forEach((article) => {
console.log(`${article.data.title}:`);
console.log(` Comments: ${article._count.comments}`);
console.log(` Likes: ${article._count.likes}`);
console.log(` Tags: ${article._count.tags}`);
});Many-to-many Includes (getDataRecord), Use getDataRecord with an include to fetch a single record together with its many-to-many relations and per-relation pagination:
// Include many-to-many relations with pagination via getDataRecord.
// Many-to-many includes (REFERENCE_MANY / MEMBER_REFERENCE_MANY) are served
// only on a single-record read, so use getDataRecord with an include — not
// queryDataRecords, which lists records and rejects these includes.
const { data: record } = await memberstack.getDataRecord({
table: 'articles',
recordId: 'rec_article_123',
include: {
tags: { take: 10 }, // REFERENCE_MANY (records)
likedBy: { take: 25 } // MEMBER_REFERENCE_MANY (members)
}
});
// Included relations are returned under record.data.<relation> as { records, pagination }
// Tags pagination: cursor is numeric internalOrder
console.log('tags.hasMore:', record.data.tags.pagination.hasMore);
console.log('tags.endCursor:', record.data.tags.pagination.endCursor);
// likedBy pagination: cursor is ISO date string createdAt
console.log('likedBy.hasMore:', record.data.likedBy.pagination.hasMore);
console.log('likedBy.endCursor:', record.data.likedBy.pagination.endCursor);Standard list queries with queryDataRecords support includes for simple relations only (REFERENCE, MEMBER_REFERENCE). To include many-to-many relations (REFERENCE_MANY, MEMBER_REFERENCE_MANY), use getDataRecord with an include — a single-record read. Nested includes are not supported.
Managing Relationships, Connect and disconnect related records:
// Add and remove tags from an article
await memberstack.updateDataRecord({
recordId: 'rec_article_123',
data: {
tags: {
connect: [
{ id: 'rec_tag_advanced' },
{ id: 'rec_tag_tutorial' }
],
disconnect: [
{ id: 'rec_tag_beginner' }
]
}
}
});
// Add current member to favorites
await memberstack.updateDataRecord({
recordId: 'rec_article_123',
data: {
favoritedBy: {
connect: [{ self: true }]
}
}
});Access Control
Data Tables enforce table-level access rules that determine who can create, read, update, and delete records.
- Create Rule: Who can create new records in this table
- Read Rule: Who can view records (filters applied automatically)
- Update Rule: Who can modify existing records
- Delete Rule: Who can remove records from the table
// Access rules are enforced automatically
try {
// This will only return records the current member can access
const { data } = await memberstack.queryDataRecords({
table: 'private_notes',
query: {
orderBy: { createdAt: 'desc' }
}
});
console.log('Accessible records:', data.records.length);
} catch (error) {
if (error.statusCode === 403) {
console.log('Access denied - insufficient permissions');
}
}1. Optimize Your Queries: Use specific filters to reduce data transfer; implement pagination for large datasets; use count queries when you only need totals; prefer cursor-based pagination over skip/offset.
2. Handle Relationships Efficiently: Use includes judiciously - only fetch what you need; use relationship counts instead of loading full collections; consider separate queries for complex relationship data.
3. Respect Rate Limits: Batch operations when possible; cache frequently accessed data; implement exponential backoff for retries; use count queries to avoid unnecessary data fetching.
4. Error Handling: Always handle access control errors (403); implement proper validation for required fields; handle network errors and retry appropriately.
- Many-to-many includes (REFERENCE_MANY, MEMBER_REFERENCE_MANY) require a single-record read via
getDataRecord(with aninclude), notqueryDataRecords - Deep nested includes are not supported
- Access rules are enforced automatically and cannot be overridden
- Rate limits vary by operation type - plan your usage accordingly
- Field uniqueness constraints are not returned in table schemas
- BigInt values are converted to Numbers in responses
- GET
/v1/data-recordssupports only:tableKey,createdAfter,createdBefore,sortBy,sortDirection,limit,after. UsequeryDataRecordsfor field-level filters and includes.
Protected Content
There are several ways to protect content with Memberstack. The simplest approach is using getCurrentMember to securely display member-specific content. For page-level protection, the implementation will vary based on your framework.
The most straightforward way to show protected content is to use getCurrentMember and render based on the returned data:
// Simple protected content example
async function showMemberContent() {
try {
const { data: member } = await memberstack.getCurrentMember();
if (member) {
// Member is logged in, show their data
document.getElementById('memberName').textContent = member.customFields.name;
document.getElementById('memberPlan').textContent =
member.planConnections?.[0]?.planId || 'No plan';
// Check plan access
const hasPremium = member.planConnections?.some(plan =>
plan.planId === 'premium_plan_id' && plan.status === 'ACTIVE'
);
// Show/hide premium content
document.getElementById('premiumContent').style.display =
hasPremium ? 'block' : 'none';
} else {
// Not logged in, show public content only
document.getElementById('publicContent').style.display = 'block';
}
} catch (error) {
console.error('Error:', error);
}
}The getCurrentMember approach above hides content in the browser, which is fine for UX but not a security boundary. For content that must stay private, use getSecureContent: Memberstack validates the member's access server-side and only returns the content if they're allowed, so it can't be bypassed client-side. Use getRestrictedUrlGroups to read the gated-content rules configured in your dashboard (Gated Content).
// List the app's restricted URL groups (your gated-content rules).
// No login required, this is app configuration.
const { data: groups } = await memberstack.getRestrictedUrlGroups();
groups.forEach((group) => {
console.log(group.name, "requires plans:", group.plans.map(p => p.id));
console.log(" protected URLs:", group.urls.map(u => u.url));
});
// Fetch a specific gated content block. Access is validated SERVER-SIDE, so it
// can't be bypassed on the client. The member must be logged in and have access.
async function loadSecureContent(contentId) {
try {
const { data } = await memberstack.getSecureContent({ contentId });
// data.type is "HTML" | "JSON" | "TEXT" | ...; data.content is the payload
document.getElementById("gated").innerHTML = data.content;
} catch (error) {
// "access-denied" if the member isn't allowed (or isn't logged in)
console.error("No access:", error.message);
}
}getRestrictedUrlGroups() resolves to { data: [...] } (an array of groups) and needs no login. getSecureContent({ contentId }) resolves to { data: { content, type } } and throws access-denied when the member lacks access.
App Configuration
Like getRestrictedUrlGroups(), getApp() reads app-level configuration and requires no login. It resolves to { data: { id, name, mode, plans, contentGroups, emailVerificationEnabled, requireEmailVerification, customField, branding, authProviders } }, useful for discovering enabled OAuth providers, available plans, gated-content groups, and whether email verification is required.
// Read your app's public configuration. No login required.
const { data: app } = await memberstack.getApp();
console.log("App name:", app.name);
console.log("Mode:", app.mode); // "live" | "sandbox"
console.log("Email verification enabled:", app.emailVerificationEnabled);
console.log("Verification required:", app.requireEmailVerification);
// Available plans and gated-content groups configured in the dashboard:
app.plans.forEach((plan) => console.log("Plan:", plan.id, plan.name));
app.contentGroups.forEach((group) => console.log("Content group:", group.name));
// OAuth providers enabled for this app:
app.authProviders.forEach((p) => console.log("Provider:", p.provider));
// Other fields: app.id, app.customField (defined custom fields), app.brandingFramework-Specific Page Protection
While Memberstack works with any framework, we recommend consulting framework-specific resources or a Memberstack Expert for detailed implementation guidance in Sveltekit, Next.js, Vue/Nuxt, etc.
SvelteKit
Here's how to protect routes in SvelteKit using server-side hooks. This approach is secure and efficient since authentication checks happen before any page data is loaded.
You'll need to set up these files:
- src/hooks.server.ts - Main authentication logic
- src/app.d.ts - TypeScript definitions (optional)
- src/routes/+layout.server.ts - Share auth data with all routes
Here's the main authentication hook implementation:
// src/hooks.server.ts
import { redirect } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const path = event.url.pathname;
// Define public and protected paths
const publicPaths = ['/login', '/signup', '/reset-password'];
const protectedPaths = ['/dashboard', '/settings', '/profile'];
// Skip auth check for public paths
if (publicPaths.some(p => path.startsWith(p))) {
return await resolve(event);
}
// Check if current path is protected
if (protectedPaths.some(p => path.startsWith(p))) {
try {
const { data: member } = await memberstack.getCurrentMember();
if (!member) {
throw redirect(303, '/login');
}
// Add member to event.locals
event.locals.member = member;
} catch (error) {
console.error('Auth check failed:', error);
throw redirect(303, '/login');
}
}
return await resolve(event);
};Add these types to src/app.d.ts for better TypeScript support:
// src/app.d.ts
declare global {
namespace App {
interface Locals {
member: {
id: string;
email: string;
// Add other member properties
} | null;
}
}
}After setting up the hook, you can access the member data in any server load function:
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
export const load = (async ({ locals }) => {
// Member is already checked in hooks.server.ts
return {
member: locals.member
};
}) satisfies PageServerLoad;- Centralized authentication logic in one place
- Server-side protection for better security
- Early redirects before page data is loaded
- TypeScript support for better development experience
- Easy to maintain and update protected routes list
Next.js
Next.js offers two approaches to routing: the App Router (modern) and Pages Router (legacy). Here's how to implement authentication in both:
You'll need to set up these files:
- middleware.ts - Global authentication middleware (App Router)
- types/next-auth.d.ts - TypeScript definitions (optional)
- components/AuthProvider.tsx - Auth context for client components
Here's how to protect routes using the modern App Router approach:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
// Define protected paths
const protectedPaths = ['/dashboard', '/settings', '/profile'];
const path = request.nextUrl.pathname;
// Skip auth check for public paths
if (!protectedPaths.some(p => path.startsWith(p))) {
return NextResponse.next();
}
try {
// Get member token from cookies
const token = request.cookies.get('memberstack')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
} catch (error) {
console.error('Auth middleware error:', error);
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Configure which paths the middleware runs on
export const config = {
matcher: [
'/dashboard/:path*',
'/settings/:path*',
'/profile/:path*'
],
};Add these types for better TypeScript support:
// types/next-auth.d.ts
declare module 'next-auth' {
interface User {
id: string;
email: string;
customFields?: Record<string, string>;
}
}
declare module 'next' {
interface Request {
member?: {
id: string;
email: string;
customFields?: Record<string, string>;
}
}
}For older projects using the Pages Router, use getServerSideProps:
// pages/protected.js
export async function getServerSideProps({ req }) {
// Initialize Memberstack
const memberstack = memberstackDOM.init({
publicKey: process.env.NEXT_PUBLIC_MEMBERSTACK_PUBLIC_KEY,
useCookies: true
});
try {
const { data: member } = await memberstack.getCurrentMember();
if (!member) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
// Important: Serialize the member object for SSR
return {
props: {
member: JSON.parse(JSON.stringify(member))
}
}
} catch (error) {
console.error('Auth check failed:', error);
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
}- Always serialize member data in getServerSideProps using JSON.parse(JSON.stringify())
- Use middleware for App Router and getServerSideProps for Pages Router
- Remember to handle both client and server-side authentication states
- Consider using an AuthProvider component for sharing auth state
Here's a helpful AuthProvider component for managing auth state:
// components/AuthProvider.tsx
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import memberstackDOM from "@memberstack/dom"
const AuthContext = createContext({
member: null,
isLoading: true,
memberstack: null
})
export function AuthProvider({ children }) {
const [member, setMember] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [memberstack, setMemberstack] = useState(null)
useEffect(() => {
const ms = memberstackDOM.init({
publicKey: process.env.NEXT_PUBLIC_MEMBERSTACK_PUBLIC_KEY,
useCookies: true
})
setMemberstack(ms)
// onAuthChange returns an object: destructure { unsubscribe }.
// The callback receives the member object directly (or null).
const { unsubscribe } = ms.onAuthChange((member) => {
setMember(member)
setIsLoading(false)
})
return () => unsubscribe()
}, [])
return (
<AuthContext.Provider value={{ member, isLoading, memberstack }}>
{children}
</AuthContext.Provider>
)
}
// Helper hook to use auth context
export const useAuth = () => useContext(AuthContext)App Router Benefits
- Middleware runs before page loads
- Better performance with streaming
- Server Components by default
- Simpler data fetching
Pages Router Benefits
- Simpler mental model
- More examples available
- Better backward compatibility
- Stable API surface
Vue/Nuxt
For Vue and Nuxt applications, authentication is handled through middleware and composables:
// composables/useAuth.ts
const useAuth = () => {
const member = ref(null)
const isLoading = ref(true)
const memberstack = memberstackDOM.init({
publicKey: import.meta.env.VITE_MEMBERSTACK_PUBLIC_KEY,
useCookies: true
})
onMounted(() => {
memberstack.onAuthChange((newMember) => {
member.value = newMember
isLoading.value = false
})
})
return { member, isLoading, memberstack }
}Implement the authentication middleware:
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to) => {
// Skip for public routes
const publicRoutes = ['/login', '/signup', '/reset-password']
if (publicRoutes.includes(to.path)) return
const { memberstack } = useAuth()
try {
const { data: member } = await memberstack.getCurrentMember()
if (!member) {
sessionStorage.setItem('redirectPath', to.fullPath)
return navigateTo('/login')
}
} catch (error) {
console.error('Auth check failed:', error)
return navigateTo('/login')
}
})- Store auth state in a composable for reusability
- Use middleware to protect routes automatically
- Remember redirect paths for better UX
- Handle auth errors gracefully
- Use getCurrentMember for simple content protection - it's secure and easy to implement
- Implement proper loading states while checking member status
- Consider framework-specific protection for entire pages
- Always validate access client-side AND server-side for sensitive operations
- Show clear messages when access is denied
Password Management
Help members change their passwords securely. This code creates a password update form with strength checking and helpful feedback.
<!-- Add this HTML to your password management page -->
<form id="passwordForm">
<!-- Current password is needed for security -->
<div class="password-field">
<label for="currentPassword">Current Password</label>
<input
type="password"
id="currentPassword"
required
placeholder="Enter your current password"
/>
</div>
<!-- New password section -->
<div class="password-field">
<label for="newPassword">New Password</label>
<input
type="password"
id="newPassword"
required
placeholder="Enter your new password"
/>
<!-- This will show how strong the password is -->
<div id="passwordStrength"></div>
</div>
<!-- Confirm new password -->
<div class="password-field">
<label for="confirmPassword">Confirm New Password</label>
<input
type="password"
id="confirmPassword"
required
placeholder="Enter your new password again"
/>
</div>
<button type="submit">Update Password</button>
<div id="passwordMessage"></div>
</form>
// Add this JavaScript to handle password updates
document.getElementById('passwordForm').addEventListener('submit', async (event) => {
event.preventDefault();
const messageDiv = document.getElementById('passwordMessage');
// Get the password values
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// Make sure the new passwords match
if (newPassword !== confirmPassword) {
messageDiv.textContent = 'New passwords do not match';
messageDiv.style.color = 'red';
return;
}
try {
// Try to update the password
await memberstack.updateMemberAuth({
oldPassword: currentPassword,
newPassword: newPassword
});
// If successful, show a message and clear the form
messageDiv.textContent = 'Password updated successfully!';
messageDiv.style.color = 'green';
document.getElementById('passwordForm').reset();
} catch (error) {
messageDiv.textContent = error.message || 'Could not update password. Please try again.';
messageDiv.style.color = 'red';
}
});
// Add this to check password strength as they type
document.getElementById('newPassword').addEventListener('input', (event) => {
const password = event.target.value;
const strengthDiv = document.getElementById('passwordStrength');
// Check how strong the password is
let strength = 'Weak';
let color = 'red';
if (password.length >= 8) {
const hasLetter = /[a-zA-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSymbol = /[^a-zA-Z0-9]/.test(password);
if (hasLetter && hasNumber && hasSymbol) {
strength = 'Strong';
color = 'green';
} else if (hasLetter && (hasNumber || hasSymbol)) {
strength = 'Medium';
color = 'orange';
}
}
// Show the strength
strengthDiv.textContent = `Password Strength: ${strength}`;
strengthDiv.style.color = color;
});- Require the current password for security
- Check that new passwords match
- Show password strength in real-time
- Provide clear feedback on requirements
- Confirm successful updates
Account Deletion
Let members delete their own account with deleteMember(). It takes no arguments and acts on the currently logged-in member. This is irreversible: the member is logged out, their account is permanently removed, and any active Stripe subscriptions are cancelled (their Stripe customer record is retained). It resolves to { data: { id } }.
// Permanently delete the CURRENT member's account (must be logged in).
// Irreversible: the member is logged out and any active Stripe subscriptions
// are cancelled. Takes no arguments.
async function deleteAccount() {
if (!confirm("Delete your account? This cannot be undone.")) return;
try {
const { data } = await memberstack.deleteMember();
console.log("Deleted member:", data.id);
// The SDK clears the session for you (getCurrentMember now returns null),
// but it does NOT redirect, do that yourself.
window.location.href = "/goodbye";
} catch (error) {
// If self-deletion is disabled for the app, this rejects with the message
// "Please contact the website owner to delete your account."
alert("Couldn't delete account: " + error.message);
}
}- The SDK clears the session for you, but does not redirect, handle navigation yourself.
- If self-deletion is disabled for your app, the call rejects (HTTP 400) with the message "Please contact the website owner to delete your account." Enable member self-deletion under Settings → Application → Security.
- Always wrap deletion in an explicit confirmation step.