Skip to content

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

  1. Subdomain extraction: The first part of the hostname is treated as the agency slug (e.g. agency-779519 from agency-779519.daramex.org).
  2. API resolution: GET /identity/agency/id?slug=agency-779519 returns { id, name }. When no slug is provided (e.g. on main panel), the API returns the default agency.
  3. Context propagation: The resolved agency is stored in AgencyProvider and synced to the auth store for the x-agency-id header on API requests.

Email Verification → Subdomain Redirect

When a user completes email verification (e.g. after registering as an agency owner), the panel:

  1. Receives tokens from POST /identity/auth/user/verify-email.
  2. Uses the access token to fetch the organization via GET /identity/organization/my and obtain the agency slug.
  3. If the current hostname is not the correct agency subdomain, redirects to https://{slug}.{rootDomain}/auth?access_token=...&refresh_token=....
  4. 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-email must succeed.
  • If found: true, the flow advances to login. If found: 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=login without a prior successful check-email with found: true is 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/business instead 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

EndpointMethodAuthDescription
/identity/agency/idGETPublicResolve agency by slug. Query param: slug. Omit slug to get the default agency.
/identity/organization/myGETAuthGet 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:

  1. Subdomain resolution: When on an agency subdomain, the resolved agency ID.
  2. Auth token: When the user is logged in, the JWT agencyId or organization's agency.
  3. Register response: When a user registers via POST /identity/auth/user/register/agency, the response includes agencyId; the panel stores it so the subsequent verify-email call can send the correct header.
  4. Auth store: Synced by AgencyProvider and useOrganization when 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.