Majik User is a framework-agnostic, self-defending user domain model for managing identity, profile data, verification state, and application settings in modern apps and websites. It’s designed to be extensible, serializable, and safe by default, acting a
npm install @thezelijah/majik-user !GitHub Sponsors
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.
!npm !npm downloads !npm bundle size  !TypeScript
---
- Majik User
- Why Majik User?
- Features
- Security \& Integrity
- Core User Management
- Rich Profile Metadata
- Verification System
- Settings \& Restrictions
- Serialization \& Interop
- Developer Ergonomics
- Installation
- Using Cloudflare Workers?
- Usage
- Initializing a New User
- Updating User Data
- Security in Action
- Basic Info
- Profile Metadata
- Birthdate
- Address
- Social Links
- Verification Methods
- Restricting a user
- Reading Computed Properties
- Validation
- Serialization \& Parsing
- Serialize for storage
- Parse from JSON
- Public-safe JSON (no sensitive data)
- Extending Majik User
- Data Integrity \& Security
- Security Guarantees \& Non-Guarantees
- What Majik User guarantees
- What Majik User does NOT guarantee
- Supabase Integration (Optional)
- Public Signup (POST /api/users)
- Why this works well
- Fetching a User by ID (GET /api/users/:id)
- Notes
- Updating a User (PUT /api/users/:id)
- Why this pattern is recommended
- Philosophy
- Threat Model – Majik User
- Assets Protected
- Trust Boundaries
- In-Scope Threats
- 1. Cross-Site Scripting (XSS)
- 2. Unsafe URL Injection
- 3. Accidental Data Exposure
- 4. State Mutation \& Integrity Bugs
- Out-of-Scope Threats
- Assumptions
- Security Posture Summary
- Contributing
- License
- Author
- About the Developer
- Contact
---
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)
---
https, base64, etc.), blocking javascript: injection.createdAt, lastUpdate)isFullyVerified statustoJSON() for database persistencefromJSON() for hydrationtoPublicJSON() for safe public exposuretoSupabaseJSON() and fromSupabase() helpers---
``bash`
npm install @thezelijah/majik-user
Majik 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):
`json`
{
"$schema": "node_modules/wrangler/config-schema.json",
"compatibility_date": "2025-09-27",
"compatibility_flags": ["nodejs_compat"]
}
---
`ts`
import { MajikUser } from "@thezelijah/majik-user";
`ts
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:
`ts
const generatedID: string = customIDGenerator();
const user = MajikUser.initialize(
"business@thezelijah.world",
"Zelijah",
generatedID
);
`
#### Security in Action
`ts
// 1. Protection against XSS
try {
user.displayName = " 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 coding ");
console.log(user.metadata.bio);
// Output: "I love coding" (Harmful tags stripped automatically)
`
#### Basic Info
`ts
user.email = "business@majikah.solutions";
user.displayName = "Josef";
`
> Changing email or phone automatically marks them as unverified.
#### Profile Metadata
`ts
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");
`
#### Birthdate
`ts
user.setBirthdate("1995-10-26");
// or
user.setBirthdate(new Date("1995-10-26"));
`
You can then access:
`ts
user.age; // number | null
user.birthday; // YYYY-MM-DD | null
`
#### Address
`ts
user.setAddress({
street: "123 Main St",
city: "Manila",
country: "PH",
});
`
You can then access:
`ts
user.address; // "123 ABC St, Manila, PH"
`
#### Social Links
`ts
user.setSocialLink("Instagram", "https://instagram.com/thezelijah");
user.removeSocialLink("Instagram");
`
---
`ts
user.verifyEmail();
user.verifyPhone();
user.verifyIdentity();
user.isEmailVerified;
user.isFullyVerified;
`
You can also unverify:
`ts
user.unverifyEmail();
user.unverifyPhone();
user.unverifyIdentity();
`
---
`ts
// Restrict indefinitely
user.restrict();
// Restrict until a specific date
user.restrict(new Date("2026-01-01"));
``ts
user.isCurrentlyRestricted(); // boolean
`
To remove restriction:
`ts
user.unrestrict();
`
---
`ts
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.
`ts
const result = user.validate();
if (!result.isValid) {
console.log(result.errors);
}
`
This validates:
- Required fields
- Email format
- Dates
- Phone number format
- Birthdate format
---
#### Serialize for storage
`ts
const json = user.toJSON();
`
This output is safe to store in:
- SQL
- NoSQL
- APIs
- Files
#### Parse from JSON
`ts
const user = MajikUser.fromJSON(json);
// or
const user = MajikUser.fromJSON(jsonString);
`
#### Public-safe JSON (no sensitive data)
`ts
const publicUser = user.toPublicJSON();
`
Includes only:
- id
- displayName
- picture
- bio
- createdAt
Perfect for feeds, comments, and public profiles.
---
`ts
interface MyAppUserMetadata extends UserBasicInformation {
role: "admin" | "user";
subscriptionTier?: string;
}
class MyAppUser extends MajikUser
//Example
const user = MajikUser.initialize
"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.
usage)
- Replacement for frontend escaping, CSP, or framework-level protections
- Defense against logic bugs or application-level vulnerabilitiesMajik User is intended to be used as one layer in a defense-in-depth strategy.
---
Supabase Integration (Optional)
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:
1. Let Supabase create and authenticate the user
2. Convert the Supabase user →
MajikUser
3. Store, validate, update, and serialize using MajikUser
$3
This example shows a public signup endpoint using Supabase Auth with email/password, followed by normalization into a
MajikUser.`ts
// POST /api/users (Public Signup)
router.post('/', async (request, env: Env): Promise => {
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');
}
});
`#### Why this works well
- 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
$3
This endpoint retrieves a user directly from Supabase Admin, then converts it into a MajikUser.
`ts// GET /api/users/:id
router.get('/:id', async (request, env: Env): Promise => {
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);
});
`#### Notes
- Keeps Supabase-specific logic at the edge
- Everything beyond this point deals only with MajikUser
- Ideal for admin panels, dashboards, or internal APIs
$3
This example demonstrates safe user updates using MajikUser validation before writing back to Supabase.
`ts// PUT /api/users/:id
router.put('/:id', async (request, env: Env): Promise => {
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.,
);
});
`#### Why this pattern is recommended
- 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
---
Philosophy
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
---
Threat Model – Majik User
$3
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())---
$3
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.
---
$3
#### 1. Cross-Site Scripting (XSS)
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.
innerHTML without escaping)#### 2. Unsafe URL Injection
Threat
Injection of malicious URI schemes such as
javascript: or data: URLs.Mitigations
- Protocol allowlisting (
https, controlled data usage)
- URL parsing and validation before persistence#### 3. Accidental Data Exposure
Threat
Sensitive fields being unintentionally exposed to public APIs or clients.
Mitigations
- Explicit separation of internal vs public serialization
-
toPublicJSON()` exposes only whitelisted fields
#### 4. State Mutation & Integrity Bugs
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