IntermediateAuthentication

User Registration

Schema validation, password hashing, duplicate detection, hashed tokens, and email verification.

Click to expand
Flow StartAPI POSTData MapperdataMapperData ValidatorinputValidatorTRUEFALSESimple OutputError 400Coderegister_dataConditionIFELSESimple OutputError 400Query Dataexisting_userConditionIFELSESimple Output409 ConflictWrite DatausersHTTP RequestResend APISimple OutputSuccess 200

User Registration

Build a secure registration flow with schema validation, password strength enforcement, duplicate email detection, hashed verification tokens, token expiration, and email verification.

What You'll Build

A hardened API endpoint that:

  • Receives name, email, and password in a POST request body
  • Validates the request schema and password strength using a Data Validator (all fields required, email regex, password pattern)
  • Normalizes email (lowercase, trimmed)
  • Checks for duplicate emails in the database
  • Hashes the password with bcrypt
  • Generates a verification token and hashes it with SHA-256 before storing
  • Sets token_expires_at to 24 hours in the future
  • Sends a verification email via Resend
  • Returns generic error messages to prevent email enumeration

Endpoint: POST /api/v1/YOUR_ID/register

Request body:

{
  "name": "John Doe",
  "email": "user@example.com",
  "password": "MyP@ssw0rd!"
}

Responses:

Scenario Status Body
Success 200 { "success": true, "message": "Registration successful. Please check your email." }
Invalid schema / weak password 400 { "valid": false, "errors": [...] }
Duplicate email 409 { "success": false, "error": "An account with this email already exists" }

Prerequisites

Table: users

Column Type Description
email text User's email (unique)
password text Bcrypt hash of the password
name text User's display name
verified text "yes" or "no"
verification_token text SHA-256 hash of the raw token sent in the email
token_expires_at datetime When the verification token expires (24h after registration)
verified_at datetime Timestamp of when the account was verified

Workflow Flow

Flow Start (API POST /register)
  ↓
Data Mapper                     → dataMapper
  ↓
Data Validator                  → inputValidator
  ├─ FALSE → Simple Output (400 validation error)
  └─ TRUE ↓
Code Node (Prepare Data)        → register_data
  ↓
Query Data (Check Duplicate)    → existing_user
  ↓
Condition (Email Exists?)
  ├─ ELSE → Simple Output (409 duplicate)
  └─ IF ↓
Write Data (Insert User)        → write_result
  ↓
HTTP Request (Send Email)       → httpRequest
  ↓
Simple Output (200 success)

Security Improvements Over the Basic Flow

Area Before After
Schema validation None Data Validator (name, email, password required)
Email format Basic regex in code Data Validator regex pattern
Password strength Code-based checks Data Validator regex pattern
Email handling Raw Normalized (lowercase, trimmed)
Token storage Plaintext SHA-256 hashed
Token expiration None token_expires_at set to 24h
Rate limit 60/min 10/min
Timeout 30s 15s
Error messages Leak validation details Generic where needed
CORS None Restricted to production domain

Step 1 - Flow Start

Setting Value
Trigger Type API
Method POST only
Rate Limit 10 requests/minute
Timeout 15 seconds
CORS Your production domain (HTTPS only)

Step 2 - Data Mapper

Map the incoming request body fields into a clean object for the Data Validator.

Output variable: dataMapper

Template:

{
  "name": "{{name}}",
  "email": "{{email}}",
  "password": "{{password}}"
}

Step 3 - Data Validator

Validate the mapped data schema, email format, and password strength — all in one node, no code needed.

Output variable: inputValidator

Input data: {{dataMapper}}

Field Type Required Validation
name string yes minLength: 1
email string yes minLength: 5, pattern: ^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
password string yes minLength: 8, pattern: (?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])

The password pattern uses four simple lookaheads:

  • (?=.*[a-z]) — at least one lowercase letter
  • (?=.*[A-Z]) — at least one uppercase letter
  • (?=.*\d) — at least one digit
  • (?=.*[\W_]) — at least one special character

The Data Validator has two output ports:

Port Fires when Next node
TRUE All fields present, correct type, and match patterns → Step 4 (Code node)
FALSE Missing, wrong type, or invalid format → Simple Output (400 error)

Step 3F - Simple Output (Validation Error)

Connected to the Data Validator's FALSE port.

Setting Value
Status 400
Type JSON
Output {{inputValidator}}

Step 4 - Code Node (Prepare Data)

With validation handled by the Data Validator, this Code node only normalizes the email, hashes the password, generates a verification token, hashes it with SHA-256, and calculates the token expiration.

Output variable: register_data

(function() {
  var name = (variables.dataMapper.name || '').trim();
  var email = (variables.dataMapper.email || '').trim().toLowerCase();
  var password = variables.dataMapper.password || '';

  var hashedPassword = bcryptHash(password);

  // Generate raw token (sent in email) and hash it (stored in DB)
  var rawToken = uuid().replace(/-/g, '');
  var hashedToken = sha256(rawToken);

  // Token expires 24 hours from now
  var tokenExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();

  return {
    name: name,
    email: email,
    hashedPassword: hashedPassword,
    rawToken: rawToken,
    hashedToken: hashedToken,
    tokenExpiresAt: tokenExpiresAt
  };
})();

Why hash the token before storing?

The raw token is sent to the user's email. Only the SHA-256 hash is stored in the database. If the database is compromised, attackers can't use the hashed tokens to verify accounts. The verify workflow hashes the incoming token and compares it against the stored hash — same principle as password hashing.

Why set token_expires_at?

Without expiration, a verification token is valid forever. If a user's email is compromised months later, old tokens could still be used. A 24-hour window is standard — long enough for the user to check their email, short enough to limit exposure.

Step 5 - Query Data (Check Duplicate Email)

Before inserting, check if a user with this email already exists.

Output variable: existing_user

Setting Value
Source users (Structured)
Columns email
Filter email equals {{register_data.email}}
Output Format List of Rows
Limit 1

Step 6 - Condition (Email Exists?)

Branch Action
IF {{existing_user.data.totalCount}} equals 0 → Continue to Step 7 (no duplicate)
ELSE → Simple Output (409 Conflict)

ELSE branch - Simple Output (Duplicate)

Setting Value
Status 409
Type JSON
Output { "success": false, "error": "An account with this email already exists" }

Step 7 - Write Data (Insert User)

Insert the new user with all required fields, including the hashed token and expiration.

Setting Value
Source users (Insert)
Column Value
email {{register_data.email}}
password {{register_data.hashedPassword}}
name {{register_data.name}}
verified no
verification_token {{register_data.hashedToken}}
token_expires_at {{register_data.tokenExpiresAt}}

The password stored is the bcrypt hash, never the plain text. The verification token stored is the SHA-256 hash — the raw token is only sent in the email. The verified field starts as "no".

Step 8 - HTTP Request (Send Verification Email)

Send a verification email using the Resend API. The raw (unhashed) token is sent in the email link.

Setting Value
Method POST
URL https://api.resend.com/emails
Auth Bearer {{secrets.resend_api_key}}
Header Content-Type: application/json

Body:

{
  "from": "YourApp <noreply@yourdomain.com>",
  "to": "{{register_data.email}}",
  "subject": "Verify your account",
  "html": "<h2>Welcome, {{register_data.name}}!</h2><p>Click below to verify your email:</p><p><a href='https://yourdomain.com/verify?token={{register_data.rawToken}}&email={{register_data.email}}'>Verify my email</a></p><p>This link expires in 24 hours.</p><p>If you didn't create this account, you can ignore this email.</p>"
}

The email link contains the raw token (rawToken), not the hashed one. When the user clicks the link, the verify workflow hashes it and compares against the stored hash. Store your Resend API key in the Vault (Settings → Vault) as resend_api_key.

Step 9 - Simple Output (Success)

Setting Value
Status 200
Type JSON
Output { "success": true, "message": "Registration successful. Please check your email." }

Post-Import Setup

After importing this workflow, you need to configure:

  1. Flow Start node — Update CORS origins to your production domain
  2. Data Mapper — Already configured, no changes needed
  3. Data Validator — Verify that Input Data is set to {{dataMapper}}
  4. Query Data (Check Duplicate) — Connect to your users table and map the email column
  5. Write Data (Insert User) — Connect to your users table and map all column IDs
  6. Vault — Store your Resend API key as resend_api_key in Settings → Vault
  7. HTTP Request — Update the from address and verification URL domain

Frontend Integration

async register(name: string, email: string, password: string) {
  const response = await fetch('https://your-domain.com/api/v1/YOUR_ID/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, email, password })
  });
  return response.json();
}

Testing

Test 1 - Successful registration:

curl -X POST https://your-domain.com/api/v1/YOUR_ID/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Test User", "email": "test@example.com", "password": "MyP@ssw0rd!"}'

Expected: { "success": true, "message": "Registration successful. Please check your email." }

Test 2 - Missing fields (caught by Data Validator):

curl -X POST https://your-domain.com/api/v1/YOUR_ID/register \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com"}'

Expected: 400 with validator error details

Test 3 - Weak password (caught by Data Validator):

curl -X POST https://your-domain.com/api/v1/YOUR_ID/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Test", "email": "new@example.com", "password": "weak"}'

Expected: 400 with validator error (password doesn't match pattern)

Test 4 - Duplicate email:

Run Test 1 again.

Expected: { "success": false, "error": "An account with this email already exists" }

What to verify in the database

After a successful registration, check users:

Column Expected
email The email you sent (lowercase)
password A bcrypt hash (starts with $2a or $2b)
name The name you sent
verified no
verification_token A 64-character hex string (SHA-256 hash)
token_expires_at A timestamp 24 hours in the future

Security Checklist

Control Status
POST only (no GET)
Schema validation (Data Validator)
Email format validation (regex pattern)
Password strength enforcement (regex pattern in Data Validator)
Email normalization (lowercase, trim)
Password hashed with bcrypt
Duplicate email check
Verification token hashed (SHA-256) before storage
Token expiration set (24h)
Raw token only in email, never stored
Rate limiting (10/min)
Reduced timeout (15s)
API key stored in Vault
No real secrets in tutorial JSON