Majik User is a framework-agnostic, security-hardened user domain model for modern applications. It provides a strongly typed foundation for managing identity, profile data, and settings, that enforces plain-text input, aggressively validates user-controlled fields, and reduces XSS risk by default through strict domain-level policies.
This package is designed to be the isomorphic source of truth—ensuring that user data remains clean, validated, and secure as it moves between your frontend, backend, and database.
- Majik User
Secure by Default: Enforces a strict plain-text input policy, protocol-safe URI validation, and defensive sanitization for externally sourced data—reducing XSS risk at the domain layer.
Most apps scatter user logic across:
- database schemas
- auth provider objects
- API DTOs
- frontend state
Majik User centralizes all of that logic into one predictable, reusable domain object.
It is:
- Strongly typed (TypeScript-first)
- Serializable and persistence-ready
- Extensible via generics
- Safe by default (public vs private data)
- Compatible with Supabase (optional)
- Plain-Text Enforcement: HTML and unsafe markup are rejected or stripped from user-controlled fields.
- XSS Risk Mitigation: Optional DOMPurify integration normalizes externally sourced input into safe plain text.
- Defensive Setters: Setters validate and normalize data before it reaches internal state.
- Safe URI Enforcement: Profile pictures and social links are restricted to safe protocols (
https, base64, etc.), blocking javascript: injection. - Readonly State: Getters return deep copies or readonly versions of data to prevent accidental state mutation.
- Unique user ID generation (UUID)
- Email + display name validation
- Automatic timestamps (
createdAt,lastUpdate) - SHA-256–based hashed identifier
- Full name handling (first, middle, last, suffix)
- Profile picture, bio, phone, gender
- Birthdate with age calculation
- Address formatting
- Social links
- Language and timezone preferences
- Email verification
- Phone verification
- Identity (KYC-style) verification
- Combined
isFullyVerifiedstatus
- Notification preferences
- System-level restrictions
- Temporary or permanent account restriction
- Restriction expiration checks
toJSON()for database persistencefromJSON()for hydrationtoPublicJSON()for safe public exposuretoSupabaseJSON()andfromSupabase()helpers
- Generic metadata support
- Profile completeness scoring
- Built-in validation with error reporting
- Cloneable user instances
- Designed to be subclassed
npm install @thezelijah/majik-userMajik User uses isomorphic-dompurify for high-grade security. To run this in a Cloudflare Worker environment, you must enable Node.js compatibility in your wrangler configuration.
Add the following to your wrangler.json (or .toml):
{
"$schema": "node_modules/wrangler/config-schema.json",
"compatibility_date": "2025-09-27",
"compatibility_flags": ["nodejs_compat"]
}import { MajikUser } from "@thezelijah/majik-user";const user = MajikUser.initialize(
"business@thezelijah.world",
"Zelijah"
);What this does:
- Generates a UUID if no ID is provided
- Hashes the user ID
- Sets default metadata and settings
- Sets createdAt and lastUpdate
- Validates email and display name
You can optionally provide your own ID:
const generatedID: string = customIDGenerator();
const user = MajikUser.initialize(
"business@thezelijah.world",
"Zelijah",
generatedID
);// 1. Protection against XSS
try {
user.displayName = "<script>alert('hacked')</script> Josef";
} catch (e) {
// Throws: "Display name contains suspicious HTML tags"
}
// 2. Protocol Safety
try {
user.setPicture("javascript:alert('xss')");
} catch (e) {
// Throws: "Invalid or unsafe URL protocol detected."
}
// 3. Auto-Sanitization on Metadata
user.setMetadata("bio", "I love <b>coding</b> <img src=x onerror=alert(1)>");
console.log(user.metadata.bio);
// Output: "I love <b>coding</b>" (Harmful tags stripped automatically)user.email = "business@majikah.solutions";
user.displayName = "Josef";Changing email or phone automatically marks them as unverified.
user.setName({
first_name: "Josef",
last_name: "Fabian",
});
user.setBio("Creative technologist and builder.");
user.setPicture("https://thezelijah.world/avatar.png");
user.setPhone("+639123456789");
user.setGender("male");
user.setLanguage("en");
user.setTimezone("Asia/Manila");
user.setBirthdate("1995-10-26");
// or
user.setBirthdate(new Date("1995-10-26"));You can then access:
user.age; // number | null
user.birthday; // YYYY-MM-DD | nulluser.setAddress({
street: "123 Main St",
city: "Manila",
country: "PH",
});You can then access:
user.address; // "123 ABC St, Manila, PH"user.setSocialLink("Instagram", "https://instagram.com/thezelijah");
user.removeSocialLink("Instagram");user.verifyEmail();
user.verifyPhone();
user.verifyIdentity();
user.isEmailVerified;
user.isFullyVerified;You can also unverify:
user.unverifyEmail();
user.unverifyPhone();
user.unverifyIdentity();// Restrict indefinitely
user.restrict();
// Restrict until a specific date
user.restrict(new Date("2026-01-01"));user.isCurrentlyRestricted(); // booleanTo remove restriction:
user.unrestrict();user.fullName;
user.formattedName;
user.initials;
user.profileCompletionPercentage;
user.hasCompleteProfile();This validates not just formats, but also inspects the entire user object (including nested addresses and social links) for unsafe markup and suspicious input.
const result = user.validate();
if (!result.isValid) {
console.log(result.errors);
}This validates:
- Required fields
- Email format
- Dates
- Phone number format
- Birthdate format
const json = user.toJSON();This output is safe to store in:
- SQL
- NoSQL
- APIs
- Files
const user = MajikUser.fromJSON(json);
// or
const user = MajikUser.fromJSON(jsonString);const publicUser = user.toPublicJSON();Includes only:
- id
- displayName
- picture
- bio
- createdAt
Perfect for feeds, comments, and public profiles.
Majik User is generic-first and designed to be extended.
interface MyAppUserMetadata extends UserBasicInformation {
role: "admin" | "user";
subscriptionTier?: string;
}
class MyAppUser extends MajikUser<MyAppUserMetadata> {}
//Example
const user = MajikUser.initialize<MyAppUserMetadata>(
"business@thezelijah.world",
"Zelijah"
);Now your app has a fully typed, domain-safe user model.
Majik User ensures that your data is not only well-structured but also safe and meaningful across your entire stack.
| Feature | Description |
|---|---|
| Isomorphic | Runs everywhere—Works seamlessly in the Browser, Node.js, and Edge Functions. |
| Smart Mapping | Automatically normalizes messy, flat metadata into structured, nested objects. |
| Calculated Getters | Values like .age, .initials, and .isFullyVerified are computed on the fly. |
| XSS Risk Reduction | Plain-text enforcement and optional DOMPurify normalization reduce XSS attack surface. |
Majik User is designed to reduce risk, not to provide absolute security guarantees.
- User-controlled fields are treated as untrusted by default
- HTML and unsafe markup are rejected or normalized into plain text
- Public-facing data is intentionally limited and sanitized
- Input validation is enforced across initialization, mutation, and serialization
- Complete protection against XSS in rendering contexts
- Safety against misuse (e.g. unsafe
innerHTMLusage) - Replacement for frontend escaping, CSP, or framework-level protections
- Defense against logic bugs or application-level vulnerabilities
Majik User is intended to be used as one layer in a defense-in-depth strategy.
Majik User is designed to sit cleanly on top of Supabase Auth, acting as your domain layer while Supabase handles authentication and sessions.
The recommended pattern is:
- Let Supabase create and authenticate the user
- Convert the Supabase user →
MajikUser - Store, validate, update, and serialize using
MajikUser
This example shows a public signup endpoint using Supabase Auth with email/password, followed by normalization into a MajikUser.
// POST /api/users (Public Signup)
router.post('/', async (request, env: Env): Promise<Response> => {
console.log('[POST] /users/');
const errorResponse = await applyMiddleware(request, env);
if (errorResponse instanceof Response) return errorResponse;
const body = (await request.json()) as API_SUPABASE_SIGN_UP_BODY;
if (!body?.email || !body.password || !body?.options?.data) {
return error('Missing required signup fields', 400, 'MISSING_FIELDS');
}
try {
const supabase = createSupabaseAPIClient(env);
const { data, error: sbError } =
await supabase.auth.signUp(body as SignUpWithPasswordCredentials);
if (sbError) {
const isDup = sbError.message.includes('already registered');
return error(
isDup ? 'This email is already registered.' : sbError.message,
isDup ? 409 : 400,
isDup ? 'EMAIL_ALREADY_EXISTS' : undefined,
);
}
// Supabase returns a user even if the email already exists
if (!data.user?.identities || data.user.identities.length <= 0) {
return error('Email already exists. Try logging in.', 409, 'EMAIL_ALREADY_EXISTS');
}
// Normalize Supabase user → MajikUser
const userJSON = MajikUser
.fromSupabase(data.user)
.toJSON();
return jsonResponse(
{
message: 'Signup successful! Check your email.',
user: userJSON,
session: data.session,
requiresEmailConfirmation: !data.session,
},
201,
corsHeaders,
);
} catch {
return error('Internal server error', 500, 'INTERNAL_ERROR');
}
});- Supabase handles authentication & sessions
- Majik User becomes your single source of truth
- You get validation, normalization, and timestamps for free
- The returned user object is safe to store or cache
This endpoint retrieves a user directly from Supabase Admin, then converts it into a MajikUser.
// GET /api/users/:id
router.get('/:id', async (request, env: Env): Promise<Response> => {
console.log('[GET] /users/:id');
const errorResponse = await applyMiddleware(request, env);
if (errorResponse instanceof Response) return errorResponse;
const { id } = request.params;
const supabase = createSupabaseAPIClient(env);
const { data, error: sbError } =
await supabase.auth.admin.getUserById(id);
if (sbError || !data?.user) {
return error('User not found', 404, 'USER_NOT_FOUND');
}
const userJSON = MajikUser
.fromSupabase(data.user)
.toJSON();
return jsonResponse(userJSON, 200, corsHeaders);
});
- Keeps Supabase-specific logic at the edge
- Everything beyond this point deals only with MajikUser
- Ideal for admin panels, dashboards, or internal APIs
This example demonstrates safe user updates using MajikUser validation before writing back to Supabase.
// PUT /api/users/:id
router.put('/:id', async (request, env: Env): Promise<Response> => {
console.log('[PUT] /users/:id');
const errorResponse = await applyMiddleware(request, env);
if (errorResponse instanceof Response) return errorResponse;
const { id } = request.params;
const body = (await request.json()) as MajikUserJSON;
// Parse incoming data
const parsedUser = MajikUser.fromJSON(body);
// Validate before persisting
const validate = parsedUser.validate();
if (!validate.isValid) {
return error('Invalid user data', 400, 'INVALID_USER_DATA');
}
const supabase = createSupabaseAPIClient(env);
// Convert domain object → Supabase-friendly metadata
const userJSON = parsedUser.toSupabaseJSON();
const { data, error: sbError } =
await supabase.auth.admin.updateUserById(id, {
user_metadata: { ...userJSON },
});
if (sbError || !data?.user) {
console.error('Update error:', sbError);
return error(sbError?.message || 'Update failed', 400, 'UPDATE_FAILED');
}
// Return updated, normalized user
const newUserJSON = MajikUser
.fromSupabase(data.user)
.toJSON();
return success(
newUserJSON,
`Update for ${newUserJSON.email} saved successfully.`,
);
});
- Incoming data is validated before persistence
- Supabase metadata stays clean and normalized
- No leaking Supabase-specific structures to clients
- MajikUser enforces consistency across all updates
Majik User is:
- ❌ Not an ORM
- ❌ Not an auth system
- ❌ Not a UI state manager
It is:
- A domain model
- A shared contract
- A single source of truth for user behavior
Majik User is responsible for protecting:
- User identity metadata (name, email, profile info)
- User-controlled profile content (bio, social links, pictures)
- Verification state (email, phone, identity flags)
- Public-facing user representations (
toPublicJSON())
Majik User operates across multiple trust boundaries:
- External clients (browsers, mobile apps)
- APIs and serverless functions
- Authentication providers (e.g. Supabase Auth)
- Databases and caches
All data crossing into the Majik User domain is treated as untrusted.
Threat
Attackers attempt to inject HTML or script content via user-controlled fields
(e.g. display names, bios, metadata).
Mitigations
- Plain-text enforcement for user-facing fields
- Rejection or normalization of unsafe markup
- Optional DOMPurify integration for external data ingestion
- Validation during initialization, mutation, and serialization
Residual Risk
- XSS is still possible if applications render user data unsafely
(e.g.
innerHTMLwithout escaping)
Threat
Injection of malicious URI schemes such as javascript: or data: URLs.
Mitigations
- Protocol allowlisting (
https, controlleddatausage) - URL parsing and validation before persistence
Threat
Sensitive fields being unintentionally exposed to public APIs or clients.
Mitigations
- Explicit separation of internal vs public serialization
toPublicJSON()exposes only whitelisted fields- Defensive defaults for getters
Threat
Unexpected mutation of internal user state causing data corruption or bypasses.
Mitigations
- Readonly getters and defensive cloning
- Controlled setters with validation
- Immutable-like update patterns
Majik User does not attempt to protect against:
- SQL injection
- Authentication bypass
- Authorization logic flaws
- CSRF
- Server-side request forgery (SSRF)
- Business logic vulnerabilities
- Client-side misuse of rendered data
These must be handled by the surrounding application and infrastructure.
- Consumers follow secure rendering practices
- Frontend frameworks escape content by default
- CSP is implemented where appropriate
- The library is used as intended (domain layer, not UI sanitizer)
Violating these assumptions may reintroduce risk.
Majik User reduces risk by:
- Shrinking the XSS attack surface
- Enforcing strict domain invariants
- Making unsafe states difficult to represent
It does not claim to eliminate vulnerabilities entirely.
Security is a shared responsibility.
If you want to contribute or help extend support to more platforms, reach out via email. All contributions are welcome!
Apache-2.0 — free for personal and commercial use.
Made with 💙 by @thezelijah
- Developer: Josef Elijah Fabian
- GitHub: https://github.com/jedlsf
- Project Repository: https://github.com/jedlsf/majik-user
- Business Email: business@thezelijah.world
- Official Website: https://www.thezelijah.world