Overview
Snapp supports multiple public domains within a single deployment.
This is not implemented through virtual instances, duplicated databases, or separate applications. Instead, Snapp relies on Better-Auth organizations as the structural boundary between domains.
Each exposed domain maps to:
- one organization
- one authentication context
- one default access scope
- one set of limits and behavioral options
All domains share the same runtime and database, while authority and control remain scoped.
Core principle
A domain maps to an organization.
This rule drives the entire model.
Every host defined in settings.yaml:
hosts:
- origin: https://example.org
- origin: https://another-domain.dev
corresponds to:
- an organization with
id = slugify(origin) - an isolated access scope
- a dedicated Better-Auth client instance
- a distinct permission graph
There is no implicit discovery or inferred mapping.
Initialization flow
On startup, Snapp performs a structured bootstrap sequence.
1. Admin bootstrap
For each admin defined in configuration:
admin:
- email: admin@example.org
username: admin
Snapp:
- creates the user if missing
- assigns the
adminrole - prints the generated password once
- does not remove existing admins
This keeps administrative access recoverable.
2. Organization bootstrap
For each configured host:
hosts:
- origin: https://example.org
Snapp ensures:
- an organization exists with
id = slugify(origin) - the original origin is stored in metadata
- the organization remains stable across restarts
Missing organizations are created during initialization.
3. Membership enforcement
All admin users are added to every organization as owner.
This prevents:
- orphan organizations
- inaccessible domains
- domains without a privileged user
4. Role materialization
Roles are stored per organization:
owneradminmember
Each role contains a serialized permission graph.
When role definitions change, Snapp:
- invalidates the in-memory auth cache
- reloads permissions on demand
- avoids relying on stale ACLs
Request-time domain resolution
For each incoming request, Snapp determines the active host:
- Reads
event.url.origin - Normalizes protocol (development vs production)
- Matches against
settings.hosts - Falls back to the first configured host if required
The resolved host defines:
- the active organization
- the Better-Auth client in use
- the applicable limits and feature flags
Requests are not processed without a resolved host.
Authentication isolation
Each host has its own Better-Auth instance.
Instances are cached by:
slugify(host.origin)
This provides isolation for:
- sessions
- cookies
- OAuth callbacks
- rate limits
- signup rules
When host options change, the related cache entry is invalidated.
Active organization enforcement
On authenticated requests:
- if no active organization is set
- Snapp assigns the organization associated with the current host
If the user is not a member of that organization:
- access is denied
- pending invitations are checked
- invitation flows are enforced
- no silent fallback is applied
Users cannot operate within a domain they do not belong to.
Shortcode resolution
Shortcodes are not strictly limited to a single organization.
Resolution follows a defined sequence.
1. Organization-scoped exact match
active = true
organizationId = current organization
shortcode = requested
This is the primary lookup path.
2. Organization-scoped case-insensitive fallback
If enabled:
disable:
lowerCaseFallback: false
The lookup remains scoped to the current organization.
3. Global fallback (cross-organization)
If no organization-level match is found:
active = true
shortcode = requested
ORDER BY createdAt ASC
LIMIT 1
This lookup is global and used as a last step.
Rationale for global fallback
Global fallback supports:
- shortcodes created before multi-domain support
- canonical links that must resolve across domains
- predictable resolution instead of silent 404s
- reuse of existing links without duplication
Only the redirect behavior is shared.
Ownership and control remain unchanged.
What remains isolated
Even when global fallback is used:
- ownership does not change
- permissions are not bypassed
- secrets are enforced
- updates are not allowed
- analytics remain tied to the original URL
- organizations stay isolated
The host resolves the link without assuming control over it.
Security enforcement
Before redirecting, Snapp enforces:
- expiration checks
- active state validation
- VirusTotal validation
- watchlist rules
- secret verification (if configured)
Redirection occurs only if all checks succeed.
Data model summary
| Concept | Scope |
|---|---|
| Users | Global |
| Organizations | Per host |
| Sessions | Per host |
| Permissions | Per organization |
| URLs | Owned by one organization |
| Shortcode resolution | Org → global fallback |
| Redirect | Stateless |
Mental model
Snapp can be viewed as:
- one runtime
- multiple domains
- a shared database
- explicit authority boundaries
- controlled cross-domain resolution
Domains remain isolated for control, while links can remain interoperable for resolution.
Configuration implications
Adding a domain:
- creates an organization
- assigns admin membership
- initializes roles
- enables access immediately
Removing a domain:
- deletes the organization
- invalidates auth caches
- leaves URLs intact but unreachable from that host
Summary
- Multi-domain support is built-in
- Organizations define boundaries
- Authentication is host-scoped
- Permissions are explicit
- Shortcode fallback follows a defined order
- No implicit data leakage occurs
The model is deterministic, observable, and reversible.