Customer Registration for E-Commerce
Learn how to build a customer registration system for your e-commerce website using Hantera’s IAM APIs. This guide covers creating customer identities, email verification, and querying customer-scoped data.
Architecture Overview
Key Pattern:
- Your e-commerce website is an OAuth client with full Hantera permissions
- Customer login accounts are IAM principals (for authentication)
- Customer business entities are Asset actors (for orders, data)
- Principal links to Customer asset via
actorIdproperty - Website uses its own credentials to query customer’s asset and orders
Why Separate Principal from Customer Asset?
Security & Flexibility:
- Email verification required before asset access (
activated: true) - Prevents querying orders by guessing email addresses
- Principal can exist before Customer asset (registration → verification → asset creation)
- Customer asset can exist before Principal (import existing customers, then allow registration)
- One Customer asset can have multiple Principals (family accounts, B2B users)
Data Architecture:
- Customer assets have auto-generated customer numbers (e.g., CUST100001)
- Customer assets have relations to orders, payments, etc.
- Principals store minimal data (name, email, phone, actorId link)
Prerequisites
- E-commerce website/app with backend API
- Hantera access token with IAM permissions
- Understanding of OAuth 2.0 flows
- Sendings API for email verification
- Asset Actors - Understanding custom asset types
Step 1: Define Customer Asset Type
Before you can create Customer assets, you must define the customer type in the Registry.
Create Registry manifest at /registry/actors/asset/types/customer:
uri: /registry/actors/asset/types/customerspec: value: graphSetName: customer defaultNumberPrefix: "CUST" relations: orders: node: order cardinality: manyApply the manifest:
h_ apply customer-type.yamlThis defines:
- graphSetName:
customer- Createsasset.customergraph node - defaultNumberPrefix:
CUST- Auto-generates customer numbers like CUST100001 - Relations: Links to orders for querying customer’s order history
Step 2: Create OAuth Client for Website
First, register your e-commerce website as an OAuth client with full order access.
PUT /resources/iam/clients/018c5f3a-1234-5678-9abc-def012345678Content-Type: application/jsonAuthorization: Bearer {adminToken}
{ "properties": { "name": "E-Commerce Website", "redirectUris": [ "https://mystore.com/auth/callback" ], "grantTypes": [ "authorization_code", "refresh_token", "client_credentials" ] }}Grant the client permissions:
PUT /resources/iam/clients/018c5f3a-1234-5678-9abc-def012345678Content-Type: application/jsonAuthorization: Bearer {adminToken}
{ "properties": { ... }, "acl": { "entries": [ { "resource": "orders:*", "permission": "read" }, { "resource": "graph:*", "permission": "query" }, { "resource": "iam:principals:*", "permission": "read" } ] }}Generate client secret:
POST /resources/iam/clients/018c5f3a-1234-5678-9abc-def012345678/secretsContent-Type: application/jsonAuthorization: Bearer {adminToken}
{ "expiresAt": "2026-12-31T23:59:59Z"}Response:
{ "secretId": "secret-uuid", "secret": "client_secret_abc123xyz", "expiresAt": "2026-12-31T23:59:59Z"}Step 3: Create Customer Role
Create a customer role as a marker (no permissions needed).
PUT /resources/iam/roles/customerContent-Type: application/jsonAuthorization: Bearer {adminToken}
{ "description": "Customer role marker for e-commerce users", "acl": { "entries": [] }}The role has no permissions because customers don’t directly access Hantera - your website does.
Step 4: Build Registration Endpoint
Create an API endpoint on your website to handle customer registration.
// POST /api/registerapp.post('/api/register', async (req, res) => { const { email, name, password } = req.body;
// Validate input if (!email || !name || !password) { return res.status(400).json({ error: 'Missing required fields' }); }
try { // Generate unique IDs const customerId = crypto.randomUUID(); const customerNumber = `CUST-${Date.now()}`; const activationCode = crypto.randomBytes(32).toString('hex');
// Create principal in Hantera const principal = await fetch(`https://<tenant url>/resources/iam/principals/${customerId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${adminToken}` }, body: JSON.stringify({ properties: { name, email, customerNumber, activationCode, activated: false }, roles: ['customer'] }) });
if (!principal.ok) { const error = await principal.json(); return res.status(principal.status).json(error); }
// Set password await fetch(`https://<tenant url>/resources/iam/principals/${customerId}/password/reset`, { method: 'POST', headers: { 'Authorization': `Bearer ${adminToken}` } });
const passwordResponse = await passwordReset.json();
// Update with actual password (not temp) await fetch(`https://<tenant url>/resources/me/password`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${userToken}` // Get token for user }, body: JSON.stringify({ currentPassword: passwordResponse.temporaryPassword, newPassword: password }) });
// Send activation email await fetch('https://<tenant url>/resources/sendings/email', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${adminToken}` }, body: JSON.stringify({ to: email, subject: 'Activate Your Account', body: { html: ` <html> <body> <h1>Welcome ${name}!</h1> <p>Click the link below to activate your account:</p> <p><a href="https://mystore.com/activate?id=${customerId}&code=${activationCode}">Activate Account</a></p> </body> </html> `, plainText: `Welcome ${name}!\n\nClick the link below to activate your account:\n\nhttps://mystore.com/activate?id=${customerId}&code=${activationCode}` }, category: 'customer_activation', dynamic: { customerId: customerId, activationCode: activationCode } }) });
res.json({ message: 'Registration successful. Please check your email to activate your account.', customerId });
} catch (error) { console.error('Registration error:', error); res.status(500).json({ error: 'Registration failed' }); }});Step 5: Email Verification Endpoint
Handle activation code verification using the IAM API.
// GET /api/activate?id=<principalId>&code=xyz123app.get('/api/activate', async (req, res) => { const { id, code } = req.query;
if (!id || !code) { return res.status(400).json({ error: 'Principal ID and activation code required' }); }
try { // Fetch principal directly from IAM API (Graph doesn't expose activationCode) const principalResponse = await fetch(`https://<tenant url>/resources/iam/principals/${id}`, { headers: { 'Authorization': `Bearer ${clientToken}` // Website's token } });
if (!principalResponse.ok) { return res.status(404).json({ error: 'Invalid activation link' }); }
const principal = await principalResponse.json();
// Verify activation code matches if (principal.properties.activationCode !== code) { return res.status(404).json({ error: 'Invalid activation code' }); }
// Check if already activated if (principal.properties.activated) { return res.json({ message: 'Account already activated. You can log in.' }); }
// Check for existing Customer asset by email const existingCustomerQuery = await fetch('https://<tenant url>/resources/graph', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${clientToken}` }, body: JSON.stringify([{ edge: 'customers', filter: `dynamic.email == '${principal.properties.email}'`, node: { fields: ['assetId', 'assetNumber'] } }]) });
const existingResult = await existingCustomerQuery.json(); let assetId;
if (existingResult[0]?.nodes?.length > 0) { // Link to existing Customer asset assetId = existingResult[0].nodes[0].assetId; console.log('Linking to existing customer asset:', assetId); } else { // Create new Customer asset const assetResponse = await fetch('https://<tenant url>/resources/actors/asset/new', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${clientToken}` }, body: JSON.stringify([{ type: 'create', body: { typeKey: 'customer', // assetNumber auto-generated (e.g., CUST100001) // Dynamic properties for the customer commands: [{ type: 'setDynamic', body: { fields: { email: principal.properties.email, name: principal.properties.name } } }] } }]) });
if (!assetResponse.ok) { const error = await assetResponse.json(); console.error('Failed to create customer asset:', error); return res.status(500).json({ error: 'Failed to create customer account' }); }
const assetResult = await assetResponse.json(); assetId = assetResult.paths[0].split('/').pop(); // Extract asset ID from path }
// Update principal: activated + link to asset const updatedProperties = { ...principal.properties, activated: true, activationCode: null, // Remove code after use actorId: assetId // Link principal to Customer asset };
await fetch(`https://<tenant url>/resources/iam/principals/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${clientToken}`, 'If-Match': principal.etag }, body: JSON.stringify({ properties: updatedProperties, roles: principal.roles }) });
res.json({ message: 'Account activated successfully! You can now log in.', assetId: assetId });
} catch (error) { console.error('Activation error:', error); res.status(500).json({ error: 'Activation failed' }); }});Step 6: Customer Login Flow
Implement OAuth login for customers.
// GET /api/auth/loginapp.get('/api/auth/login', (req, res) => { const authUrl = `https://<tenant url>/oauth/authorize?` + `client_id=${clientId}&` + `redirect_uri=${encodeURIComponent('https://mystore.com/auth/callback')}&` + `response_type=code&` + `scope=me:*`;
res.redirect(authUrl);});
// GET /api/auth/callbackapp.get('/api/auth/callback', async (req, res) => { const { code } = req.query;
try { // Exchange code for token const tokenResponse = await fetch('https://<tenant url>/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, client_id: clientId, client_secret: clientSecret, redirect_uri: 'https://mystore.com/auth/callback' }) });
const tokens = await tokenResponse.json();
// Get customer identity const identityResponse = await fetch('https://<tenant url>/resources/me', { headers: { 'Authorization': `Bearer ${tokens.access_token}` } });
const identity = await identityResponse.json();
// Check if activated if (!identity.properties.activated) { return res.status(403).json({ error: 'Please activate your account via email before logging in' }); }
// Store session req.session.customerId = identity.identityId; req.session.customerNumber = identity.properties.customerNumber; req.session.customerToken = tokens.access_token;
res.redirect('/dashboard');
} catch (error) { console.error('Login error:', error); res.status(500).json({ error: 'Login failed' }); }});Step 7: Query Customer Orders
Use customer’s customerNumber to scope order queries.
// GET /api/my-ordersapp.get('/api/my-orders', async (req, res) => { if (!req.session.customerNumber) { return res.status(401).json({ error: 'Not authenticated' }); }
try { // Website uses ITS OWN token (not customer's) const orders = await fetch('https://<tenant url>/resources/graph', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${clientToken}` // Website token, NOT customer token }, body: JSON.stringify([{ edge: 'orders', filter: `customerNumber == '${req.session.customerNumber}'`, orderBy: 'createdAt desc', node: { fields: [ 'orderId', 'orderNumber', 'createdAt', 'totalAmount', 'status' ] } }]) });
const results = await orders.json();
res.json({ orders: results[0]?.nodes || [] });
} catch (error) { console.error('Orders query error:', error); res.status(500).json({ error: 'Failed to fetch orders' }); }});Complete Flow Diagram
Customer Registration & Login Flow:
1. Registration Customer → Website: Submit email, name, password Website → Hantera IAM: Create principal (activated=false) Website → Hantera Sendings: Send activation email
2. Activation Customer → Email: Click activation link Website → Hantera IAM: Verify activation code Website → Hantera Assets: Create Customer asset (auto-generates CUST100001) Website → Hantera IAM: Update principal (activated=true, actorId=<asset>)
3. Login Customer → Website: Click login Website → Hantera OAuth: Redirect to authorization Customer → Hantera: Enter credentials Hantera → Website: Authorization code Website → Hantera OAuth: Exchange code for token Website → Hantera IAM: GET /resources/me Website: Check activated=true, store actorId in session
4. View Orders Customer → Website: Request /my-orders Website → Hantera Graph: Query asset.customer node Website → Hantera Graph: Follow orders relation Filter: Using actorId from session Hantera → Website: Return customer's orders Website → Customer: Display ordersKey Security Point: Principal cannot access Customer asset until activated: true. This prevents email-guessing attacks where someone registers with another person’s email.
Key Concepts
Customer Has No Permissions
{ "type": "principal", "properties": { "customerNumber": "CUST-12345" }, "roles": ["customer"], "acl": { "entries": [] } // Empty! No permissions.}The customer role is just a marker. All actual data access happens through the website’s OAuth client.
Website Acts as Proxy
// ❌ WRONG: Using customer's tokenAuthorization: Bearer {customerToken}
// ✅ CORRECT: Using website's tokenAuthorization: Bearer {clientToken}The website has full permissions and filters data based on customer identity properties.
Identity Properties Drive Access
// Customer's identity{ customerNumber: "CUST-12345", activated: true}
// Query scoped by propertyfilter: "customerNumber == 'CUST-12345'"Security Considerations
Validate Activation Status
Always check activated: true before allowing login:
if (!identity.properties.activated) { return res.status(403).json({ error: 'Please activate your account first' });}Secure Session Management
Store customer data in secure, HTTP-only cookies:
req.session.customerNumber = identity.properties.customerNumber;// Never expose raw Hantera tokens to frontendInput Validation
Sanitize all user input before creating principals:
const email = validator.isEmail(req.body.email) ? req.body.email : null;
if (!email) { return res.status(400).json({ error: 'Invalid email' });}Troubleshooting
Customer Can’t Log In After Registration
Problem: User registered but login fails.
Check:
- Is
activated: truein their principal properties? - Did the activation email send successfully?
- Is the activation code correct?
Orders Not Showing
Problem: Customer logged in but sees no orders.
Check:
- Is
customerNumberstored in session? - Do orders have matching
customerNumberfield? - Is website’s OAuth client token valid?
- Does client have
orders:*read permission?
Email Verification Not Working
Problem: Activation link doesn’t work.
Check:
- Does the activation URL include both
idandcodeparameters? - Is the principal ID valid (exists in IAM)?
- Does the
activationCodein the principal match the URL parameter? - Is the activation code URL-encoded properly?
Best Practices
1. Handle Existing Customer Assets
If importing existing customers, check for existing assets by email before creating a new one:
// Query for existing customer asset by emailconst existingCustomer = await fetch('https://<tenant url>/resources/graph', { method: 'POST', body: JSON.stringify([{ edge: 'customers', filter: `email == '${email}'`, node: { fields: ['assetId', 'assetNumber'] } }])});
if (existingCustomer.nodes.length > 0) { // Link principal to existing asset actorId = existingCustomer.nodes[0].assetId;} else { // Create new asset // ...}2. Expire Activation Codes
Store expiration timestamp:
properties: { activationCode: 'xyz123', activationCodeExpires: new Date(Date.now() + 24*60*60*1000).toISOString()}3. Validate Asset Creation
Always check that the Customer asset was created successfully before linking:
const assetResult = await assetResponse.json();
if (!assetResult.paths || assetResult.paths.length === 0) { console.error('Asset creation failed:', assetResult); return res.status(500).json({ error: 'Failed to create customer account' });}
const assetId = assetResult.paths[0].split('/').pop();4. Handle Email Uniqueness
Check before registration:
// Query existing customers by emailconst existing = await fetch('https://<tenant url>/resources/graph', { method: 'POST', body: JSON.stringify([{ edge: 'identities', filter: `type == 'principal' and properties.email == '${email}'` }])});
if (existing.nodes.length > 0) { return res.status(409).json({ error: 'Email already registered' });}5. Rate Limit Registration
Prevent spam registrations:
const rateLimit = require('express-rate-limit');
const registerLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5 // 5 registrations per IP});
app.post('/api/register', registerLimiter, async (req, res) => { // ...});Related Resources
- IAM Domain Model - Understanding identities and roles
- Identity Graph Node - Querying identities
- Sendings API - Sending verification emails
- OAuth 2.0 Authentication - OAuth flows