Handle System & Sigil Taxonomy
The Mallnline Handle System formalizes entity identity into a pipe-sigil taxonomy โ the platform's equivalent of Reddit's r/subreddit, Twitter's @username, and Discord's #channel. The pipe | character is the Mallnline glyph, creating a visual "doorway" that fits the virtual mall metaphor.
The Sigil Taxonomy
Every addressable entity on Mallnline has a type-specific sigil prefix:
| Sigil | Entity | Example | Route |
|---|---|---|---|
m| |
Malet | m|luminara-crafts |
/luminara-crafts |
@ |
Visitor (User) | @meekdenzo |
/u/meekdenzo |
o| |
Organization | o|acme-corp |
/orgs/acme-corp |
r| |
Malet Rep | r|sarah-luminara |
Not routable |
u| |
u-Product | u|chat |
Not routable |
Dual-Mode Resolution
Both the @ universal sigil and the pipe sigil resolve to the same entity. The @ form is for cross-platform sharing (paste in Slack, tweet about it). The pipe form is for Mallnline-native identity (in-app, community, uChat).
@meekdenzo โ mallnline.com/profile/meekdenzo (universal)
@acme-corp โ mallnline.com/orgs/acme-corp (universal)
m|luminara โ mallnline.com/luminara (pipe-only)
The `` Component
<MallHandle> is the unified rendering component for all handle displays across the platform. It replaces ad-hoc handle CSS classes with a single, type-aware, theme-responsive component.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type |
HandleType |
โ | Entity type: 'malet', 'user', 'org', 'rep', 'product' |
handle |
string |
โ | The handle value (without sigil prefix) |
linked |
boolean |
false |
Wraps in an <a> tag with auto-resolved route |
size |
'xs' | 'sm' | 'md' | 'lg' |
'sm' |
Font size tier |
Usage
<script lang="ts">
import MallHandle from '$lib/components/MallHandle.svelte';
</script>
<!-- Basic display -->
<MallHandle type="malet" handle="luminara-crafts" />
<!-- Renders: m|luminara-crafts (with indigo sigil) -->
<!-- With link -->
<MallHandle type="malet" handle="luminara-crafts" linked />
<!-- Renders: <a href="/luminara-crafts">m|luminara-crafts</a> -->
<!-- User handle -->
<MallHandle type="user" handle="meekdenzo" size="md" />
<!-- Renders: @meekdenzo (with teal sigil) -->
<!-- Organization handle -->
<MallHandle type="org" handle="acme-corp" size="xs" />
<!-- Renders: o|acme-corp (with purple sigil) -->
Design Tokens
Each entity type has a dedicated sigil color CSS variable:
| Token | Default | Used By |
|---|---|---|
--mall-handle-malet |
#667eea (Indigo) |
m| sigil accent |
--mall-handle-user |
#0d9488 (Teal) |
@ sigil accent |
--mall-handle-org |
#7c3aed (Purple) |
o| sigil accent |
--mall-handle-rep |
#d97706 (Amber) |
r| sigil accent |
--mall-handle-product |
#059669 (Emerald) |
u| sigil accent |
The component uses var(--mall-font-mono) for the monospace font stack, and adapts handle value color in dark mode via --mall-dark-secondary-text.
Size Tiers
| Size | Font Size | Best For |
|---|---|---|
xs |
0.65rem |
Data tables, sidebar items, card footers |
sm |
0.75rem |
Inline text, list items (default) |
md |
0.875rem |
Headers, profile banners |
lg |
1rem |
Hero sections, about pages |
Handle Utilities (`handleUtils.ts`)
Located at $lib/utils/handleUtils, this module provides the core functions the <MallHandle> component and future sigil-aware search depend on.
Types
type HandleType = 'malet' | 'user' | 'org' | 'rep' | 'product';
interface ParsedSigil {
type: HandleType;
handle: string;
}
`getSigilPrefix(type: HandleType): string`
Returns the canonical sigil prefix for a given entity type.
getSigilPrefix('malet') // โ 'm|'
getSigilPrefix('user') // โ '@'
getSigilPrefix('org') // โ 'o|'
getSigilPrefix('rep') // โ 'r|'
getSigilPrefix('product') // โ 'u|'
`getHandleRoute(type: HandleType, handle: string): string | null`
Resolves the URL route for a handle. Returns null for non-routable types.
getHandleRoute('malet', 'luminara-crafts') // โ '/luminara-crafts'
getHandleRoute('org', 'acme-corp') // โ '/orgs/acme-corp'
getHandleRoute('user', 'meekdenzo') // โ '/u/meekdenzo'
getHandleRoute('rep', 'sarah') // โ null
`normalizeSigil(input: string): string`
Auto-corrects mobile-unfriendly pipe alternatives to canonical form. Addresses the mobile keyboard problem โ the pipe | requires multi-tap on most mobile keyboards.
normalizeSigil('m/luminara-crafts') // โ 'm|luminara-crafts'
normalizeSigil('m:luminara-crafts') // โ 'm|luminara-crafts'
normalizeSigil('m.luminara-crafts') // โ 'm|luminara-crafts'
normalizeSigil('m|luminara-crafts') // โ 'm|luminara-crafts' (no-op)
normalizeSigil('@meekdenzo') // โ '@meekdenzo' (no pipe needed)
normalizeSigil('plain-text') // โ 'plain-text' (no sigil)
Accepted alternatives for |: /, :, .
False-Positive Guard
The normalizer validates the remainder after the 2-char sigil prefix. Handles are strictly [a-z0-9-], so any remainder containing ., @, /, :, or whitespace is clearly a URL/email/domain and is not normalized:
normalizeSigil('m.co.uk') // โ 'm.co.uk' (domain โ contains '.')
normalizeSigil('m.co@gmail.com') // โ 'm.co@gmail.com' (email โ contains '@')
normalizeSigil('m/path/to') // โ 'm/path/to' (URL path โ contains '/')
normalizeSigil('m. foo') // โ 'm. foo' (sentence โ whitespace)
normalizeSigil('m.') // โ 'm.' (bare separator)
The guard is implemented via a NON_HANDLE_CHARS regex (/[.@/::\s]/) that rejects normalization when the remainder contains characters invalid in handles.
`parseSigil(input: string): ParsedSigil | null`
Parses a raw sigil string into its type and handle components. Auto-normalizes mobile input before parsing.
parseSigil('m|luminara-crafts') // โ { type: 'malet', handle: 'luminara-crafts' }
parseSigil('@meekdenzo') // โ { type: 'user', handle: 'meekdenzo' }
parseSigil('o|acme-corp') // โ { type: 'org', handle: 'acme-corp' }
parseSigil('m/luminara') // โ { type: 'malet', handle: 'luminara' } (auto-normalized)
parseSigil('plain-text') // โ null
parseSigil('m|') // โ null (sigil without handle)
Sigil-Aware Search Integration
The search bar (Island.svelte) and search-mini.svelte are both wired into the handle system:
Search Bar Normalization
search-mini.svelte imports normalizeSigil and applies it in real-time on handleInput(), auto-correcting mobile-friendly inputs before the query reaches the suggestion engine.
Sigil-Prefix Detection
Island.svelte uses parseSigil() to detect sigil prefixes in the search query. When detected:
- The query is split into
type+handleviaparseSigil() - Content suggestions are fetched using only the
handleportion (without the sigil) - Results are filtered by entity type (
MALET,ORGANIZATION, etc.) - Product/service suggestions are suppressed (sigil mode focuses on entity search)
- The suggestion dropdown header changes from "Blogs & More" to the entity label ("Malets", "Organizations", etc.)
// Island.svelte โ sigil-aware search branching
const detected = parseSigil(normalized); // e.g. { type: 'malet', handle: 'lum' }
if (detected) {
const allContent = await searchStore.getContentSuggestions(detected.handle);
contentResults = allContent.filter(r => r.type === typeMap[detected.type]);
suggestions = { suggestions: [], categories: [], items: [] }; // clear product suggestions
}
`` Component
A dismissable CTA banner that prompts authenticated users without a handle to claim one.
Props
None โ the component reads $currentUser and $isAuthenticated from the auth store internally.
Show Condition
$isAuthenticated && $currentUser && !$currentUser.handle && !dismissed
Dismissal Logic
- Stored in
localStoragekeyhandle_claim_banner_dismissedwith the current timestamp - TTL: 7 days โ after expiry, the banner resurfaces
- Safe failure: if
localStorageis unavailable, the banner simply shows every time
Design
- Teal gradient accent (
#0d9488โ#14b8a6) matching--mall-handle-user - CTA button links to
/settings#section-profile - Slide-in animation on mount
- Dark-mode aware, responsive (CTA goes full-width on mobile)
Usage
<script lang="ts">
import HandleClaimBanner from '$lib/components/HandleClaimBanner.svelte';
</script>
<HandleClaimBanner />
Currently deployed on:
/lobbyโ between the IntentBar and FollowingFeed/profileโ above the profile card
UserMenu Handle Display
When an authenticated user has claimed a handle, their @handle is displayed in the UserMenu dropdown modal directly below the "Hi, {name}!" greeting.
The handle is rendered using <MallHandle type="user" size="sm" /> inside a teal-tinted pill (rgba(13, 148, 136, 0.08)).
{#if $currentUser.handle}
<div class="user-handle-display" data-testid="user-menu-handle">
<MallHandle type="user" handle={$currentUser.handle} size="sm" />
</div>
{/if}
Handle Claim Lifecycle
Visitors can claim a unique @handle through the Settings profile section. The lifecycle involves:
- Unclaimed state โ Visitors see "No handle claimed" with a "Claim" CTA
- Availability check โ Debounced cross-namespace validation against both user and Malet handle namespaces (parallel GraphQL requests to
checkUserHandleAvailability+checkHandleAvailability) - Claim โ
claimHandlemutation via the Nodes subgraph - Cooldown โ 30-day change cooldown enforced by
handleChangedAttimestamp - Display โ Claimed handles render via
<MallHandle type="user">in the profile banner and settings
Handle Validation Rules
| Rule | Constraint |
|---|---|
| Characters | Lowercase a-z, digits 0-9, hyphens - |
| Length | 2โ30 characters |
| Format | Must start and end with alphanumeric |
| Uniqueness | Globally unique across both user and Malet namespaces |
| Cooldown | 30 days between changes |
GraphQL Operations
# Check availability (user namespace)
query CheckUserHandleAvailability($handle: String!) {
checkUserHandleAvailability(handle: $handle)
}
# Check availability (malet namespace)
query CheckHandleAvailability($handle: String!) {
checkHandleAvailability(handle: $handle)
}
# Claim or change handle
mutation ClaimHandle($input: UpdateProfileInput!) {
updateProfile(input: $input) {
handle
handleChangedAt
}
}
Malet Handle Assignment
Malets get their handles during creation in the Malet Creation Wizard. The IdentityForm step:
- Auto-generates a handle from the Malet name (kebab-case, max 30 chars)
- Displays the
m|prefix badge using the--mall-handle-maletdesign token - Checks availability via debounced
checkHandleAvailabilityquery - Shows status with โ (available), โ (taken), or โฏ (checking) indicators
The handle lives at mallnline.com/{maletHandle} โ no prefix in the URL, just the raw handle as a top-level route parameter.
Adoption Across the Platform
The <MallHandle> component is used in these surfaces:
| Surface | File | Type | Size |
|---|---|---|---|
| Malet Header | [malet]/Header.svelte |
malet |
md |
| Malet About page | [malet]/about/+page.svelte |
malet |
md |
| Lobby Floor cards | lobby/Floor.svelte |
malet |
xs |
| Org Dashboard cards | lobby/OrgDashboard.svelte |
malet |
xs |
| Section listing | sections/[slug]/+page.svelte |
malet |
sm |
| Profile page (owned/fav) | profile/+page.svelte |
malet |
sm |
| Profile page (user badge) | profile/+page.svelte |
user |
md/sm |
| UserMenu dropdown | UserMenu.svelte |
user |
sm |
| Settings banner | settings/ProfileSection.svelte |
user |
xs/sm |
| Admin Malets table | admin/+page.svelte |
malet |
xs |
| Admin Edit History | admin/+page.svelte |
malet |
xs |
Self-Healing Profile Sync
The Mallnline backend uses federated subgraph separation โ the auth service (Rust/Postgres) manages identity (id, email, credentials), while the nodes subgraph (NestJS/MongoDB) manages profile data (handle, displayName, preferences).
When a user registers, the auth service emits a user_created TCP event to nodes, which creates the initial profile document. However, this event is fire-and-forget โ if nodes is down or the event is dropped, the user has no profile document and features like handle claims silently fail.
How the frontend self-heals
The _enrichUserProfile() function in src/lib/services/auth.ts runs after every successful auth check:
1. REST GET /me โ { id, email, username } โ auth service (Rust/Postgres)
2. authStore.setUser(baseUser) โ immediate, no handle data
3. GraphQL ME_QUERY โ { handle, handleChangedAt } โ federated gateway โ nodes
4a. Profile exists โ merge into store โ
normal path
4b. Profile MISSING โ ENSURE_PROFILE mutation โ
self-healing path
โโ nodes updateProfile auto-creates profile
Key implementation details
- Detection: If all nodes-owned fields (
handle,displayName,handleChangedAt) are undefined, the profile is assumed missing. - Auto-create: The
ENSURE_PROFILEmutation sends{ displayName: username || email_prefix }to trigger the nodesupdateProfilefind-or-create pattern. - Non-blocking: Runs in the background โ auth is not delayed by enrichment. Failures are silent.
- Idempotent: If the profile already exists,
updateProfileis a no-op update.
GraphQL operations
# Fired after every auth check to merge federated profile data
query Me {
me { id email displayName handle handleChangedAt createdAt }
}
# Fired only when no nodes profile exists (self-healing)
mutation EnsureProfile($input: UserUpdate!) {
updateProfile(input: $input) { id handle handleChangedAt displayName }
}
File Reference
| File | Purpose |
|---|---|
src/lib/components/MallHandle.svelte |
Unified sigil handle renderer component |
src/lib/components/HandleClaimBanner.svelte |
Dismissable "Claim your @handle" CTA banner |
src/lib/utils/handleUtils.ts |
Core utilities: normalizeSigil, parseSigil, getSigilPrefix, getHandleRoute |
src/lib/components/UserMenu.svelte |
User menu dropdown โ shows @handle below greeting |
src/lib/components/create-malet/IdentityForm.svelte |
Malet handle assignment during creation |
src/lib/components/settings/ProfileSection.svelte |
User handle claim/change UI |
src/routes/search/search-mini.svelte |
Search bar โ applies normalizeSigil() on input |
src/routes/lobby/Island.svelte |
Island search โ sigil-aware search branching via parseSigil() |
src/lib/queries/auth.ts |
CHECK_USER_HANDLE_AVAILABILITY, CLAIM_HANDLE, ENSURE_PROFILE queries |
src/lib/services/auth.ts |
_enrichUserProfile() โ self-healing federated profile sync |
src/lib/queries/lobby.ts |
CHECK_HANDLE_AVAILABILITY query |
tests/handleUtils.test.ts |
42 unit tests covering all 4 utility functions + false-positive guards |
tests/handleClaimBanner.test.ts |
6 unit tests covering dismissal TTL logic |
Related
- Settings Architecture โ Handle claim UI lives in the ProfileSection module
- Component Library โ Design system components that MallHandle follows
- Universal Search Index โ Sigil-aware search uses
parseSigilfor prefix matching and entity-specific filtering - uChat Client SDK โ uChat displays sender handles using the pipe sigil taxonomy
- Social Graph & Follow System โ Follow relationships surface in the Lobby alongside the HandleClaimBanner
- Public User Profiles โ Phase 2 of the Handle System โ
userByHandleresolution, activity feed, and/u/[handle]route - Invite & Notification Pipeline โ Uses
GET_USER_BY_IDto resolve member handles in org dashboards and audit logs - User Identity Resolution โ Shared
profileResolver.tsutility that batch-resolves UUIDs to@handleordisplayName