Appearance
Multi-Tenant Subdomain Resolution
The panel (and microsite) supports multi-tenancy by resolving the active agency from the hostname subdomain. When users visit agency-779519.daramex.org, the app automatically fetches the agency by slug and scopes all API requests to that tenant.
How It Works
- Subdomain extraction: The first part of the hostname is treated as the agency slug (e.g.
agency-779519fromagency-779519.daramex.org). - API resolution:
GET /identity/agency/id?slug=agency-779519returns{ id, name }. When no slug is provided (e.g. on main panel), the API returns the default agency. - Context propagation: The resolved agency is stored in
AgencyProviderand synced to the auth store for thex-agency-idheader on API requests.
Email Verification → Subdomain Redirect
When a user completes email verification (e.g. after registering as an agency owner), the panel:
- Receives tokens from
POST /identity/auth/user/verify-email. - Uses the access token to fetch the organization via
GET /identity/organization/myand obtain the agency slug. - If the current hostname is not the correct agency subdomain, redirects to
https://{slug}.{rootDomain}/auth?access_token=...&refresh_token=.... - The target subdomain's auth page detects the tokens in the URL, stores them, fetches the user, and completes login (redirect to dashboard).
This ensures users always land on their agency subdomain after verification, regardless of where they started (e.g. panel.daramex.org or a different agency subdomain).
Excluded Subdomains
The following subdomains are not treated as agency slugs and will not trigger resolution:
www,app,panel,admin,docs
Example: panel.daramex.org does not resolve an agency; the app relies on the authenticated user's agency from the JWT.
Auth Flow: Check-Email Requirement on Agency Subdomains
On agency subdomains (e.g. agency-779519.daramex.org), the auth flow only advances when check-email returns found: true:
- The user must enter an email and submit;
POST /api/identity/auth/user/check-emailmust succeed. - If
found: true, the flow advances to login. Iffound: false, the flow does not advance (registration is not available on agency subdomains). - If check-email fails (rate limit, network error, etc.), the flow does not advance.
- Direct navigation to
?step=loginwithout a prior successful check-email withfound: trueis blocked; the user is reset to the email step.
On excluded subdomains (panel, app, etc.), the flow behaves as before (login or register based on found).
Business Auth Flow
The business auth flow (/business) is triggered by the "Quiero Potenciar Mi Negocio" button. It works similarly to the agency flow but with these differences:
- First step: Shows an "En construcción" placeholder instead of a service selection grid.
- Agency ID: Always uses the agency ID from the subdomain slug, or the default agency when on the main panel (no slug).
- Registration: Uses
POST /identity/auth/user/register/businessinstead of the agency register endpoint. All other endpoints (check-email, verify-email, login) are the same.
Usage
Provider
AgencyProvider wraps the app in main.tsx and resolves the agency on mount when a valid subdomain is present.
Hook
tsx
import { useAgency } from '@/features/agency/context/use-agency';
function MyComponent() {
const { agency, slug, agencyId, isLoading, error } = useAgency();
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!agency) return <NoAgencyMessage />;
return <div>Welcome to {agency.name}</div>;
}Optional Hook
For components that may render outside the provider or when agency is optional:
tsx
import { useAgencyOptional } from '@/features/agency/context/use-agency';
function OptionalAgencyComponent() {
const context = useAgencyOptional();
if (!context) return null;
const { agency } = context;
// ...
}Helpers
getAgencySlugFromHostname()— Extracts the agency slug from the current hostname.isAgencySubdomain()— Returns true when the hostname is an agency subdomain (used for check-email enforcement).getRootDomainFromHostname()— Returns the root domain (e.g.daramex.org).buildSubdomainUrl(slug, path, searchParams?)— Builds a full URL for an agency subdomain (e.g.https://agency-779519.daramex.org/auth).
API Contract
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/identity/agency/id | GET | Public | Resolve agency by slug. Query param: slug. Omit slug to get the default agency. |
/identity/organization/my | GET | Auth | Get current user's organization (includes slug). Used after verify-email to resolve redirect target. |
The panel client treats a failed GET /identity/organization/my (network, 4xx/5xx, or empty body) as “no organization data”: it logs for debugging and returns null so the UI can show Ajustes → Organización messaging instead of breaking the shell.
Response for /identity/agency/id:
json
{
"id": "019d02fc-042e-7335-939e-ce0b2646935a",
"name": ""
}Agency ID Header
All authenticated API requests include x-agency-id when an agency is set. The value comes from:
- Subdomain resolution: When on an agency subdomain, the resolved agency ID.
- Auth token: When the user is logged in, the JWT
agencyIdor organization's agency. - Register response: When a user registers via
POST /identity/auth/user/register/agency, the response includesagencyId; the panel stores it so the subsequent verify-email call can send the correct header. - Auth store: Synced by
AgencyProvideranduseOrganizationwhen applicable.
AgencyProvider does not overwrite an existing agencyId from the auth store (e.g. from register or token) with the default agency when on the main panel (no subdomain), so the registration flow keeps the new agency ID for verify-email.