mailnix / Sub-projects (multi-tenant SaaS pattern)

    Core concepts

    Sub-projects (multi-tenant SaaS pattern)

    If you're building a SaaS app that sends transactional email on behalf of your own end-customers, sub-projects let you partition sends, traces, and quotas per downstream tenant. Your app authenticates once (as the master project), every send carries the tenant's identity, and the dashboard / webhook payloads stay per-tenant.

    When to use what

    Pick the shape that matches your tenancy model:

    RecipeUse whenNotes
    Sub-projects + customer_idThe standard B2B SaaS shape. Your app authenticates as the master, partitions traces and destinations per downstream tenant, and stamps every send with the tenant's id.Single mailnix billing relationship (yours). Per-tenant send-cap overrides. Recommended for most SaaS integrations.
    customer_id only (no sub-projects)Per-tenant attribution but every customer shares the same destination + quota pool.Cheapest. Can't isolate one tenant's bad reputation from another's. Useful for low-volume tenants on a uniform plan.
    One mailnix organization per customerYou're a payments / fintech app where every customer-of-customer is a distinct billing relationship with audit-isolation requirements (HIPAA, KYC, etc.).One Stripe customer per downstream customer. Operationally heavy.

    The rest of this guide walks the recommended sub-projects + customer_id recipe.

    The data model

      organization (your mailnix billing tenant)
           │
           ├── master_project           (your app authenticates as this)
           │       │
           │       ├── sub_project: customer-alpha
           │       ├── sub_project: customer-beta
           │       └── sub_project: customer-gamma
           │
           └── destination(s)           (SMTP / SES / Mailgun / Postmark / …)
    • The master project is what your app's backend authenticates as (one OAuth client, one set of API keys).
    • Each sub-project has its own id, dev key, and optional send-cap override. Sub-projects inherit organization_id from the master.
    • The customer_id attribute on a send is a separate axis from sub-projects: you can pass customer_id even when sending through a sub-project for clearer downstream attribution.

    The hierarchy caps at one level deep: a sub-project cannot itself be a parent.

    End-to-end integration (REST)

    The mailnix REST API is the integration surface today; there is no official mailnix client library yet. Call it from any HTTP client in your language of choice. The shape below is the canonical recipe.

    # 1. Mint a sub-project for a new tenant.
    curl -X POST https://api.mailnix.ch/v1/projects/sub \
      -H "Authorization: Bearer $MAILNIX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "parent_project_id": "proj_…master…",
        "name": "tenant:acme-corp",
        "send_cap_override": 50000
      }'
    
    # 2. Send a transactional message with per-customer attribution.
    #    customer_id is per-tenant attribution; trace lookups + webhook
    #    payloads carry it; max 128 chars (over-length is truncated).
    curl -X POST https://api.mailnix.ch/v1/messages \
      -H "Authorization: Bearer $MAILNIX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "to":          ["user@acme-corp.example"],
        "from":        "noreply@my-saas.example",
        "subject":     "Welcome",
        "html_body":   "<p>Hello.</p>",
        "customer_id": "acme-corp"
      }'
    
    # 3. Filter traces to one downstream tenant.
    curl "https://api.mailnix.ch/v1/traces?customer_id=acme-corp&limit=50" \
      -H "Authorization: Bearer $MAILNIX_API_KEY"

    The full request/response shapes live in the OpenAPI spec. For AI-agent-driven integrations the same surface is exposed via MCP at mcp.mailnix.ch.

    Routing webhooks by tenant

    When you subscribe an outbound webhook to a master project (or a sub-project), every event payload includes the customer_id you stamped at send time. A customer-side router looks like this:

    app.post("/webhooks/mailnix", verifySignature, (req, res) => {
      const event = req.body;
      // event.customer_id is the value you set on the send.
      const handler = tenantHandlers[event.customer_id] ?? defaultHandler;
      handler(event);
      res.status(204).end();
    });

    Per-tenant send-cap overrides

    By default, every sub-project shares the master project's tier quota. To budget a specific tenant differently (e.g. lift a VIP above the default, or cap an abusive tenant), set the override on the sub-project:

    curl -X PATCH https://api.mailnix.ch/v1/projects/sub/{sub_id}/quota \
      -H "Authorization: Bearer $MAILNIX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"send_cap_override": 100000}'

    Set send_cap_override to null to clear the override and fall back to the org tier default.

    Delete cascade

    Deleting a sub-project cascades to every trace, destination, and inbound route under it. The master project is not affected. Deleting the master project cascades to every sub-project as well as the master's own children. Both are irreversible; the dashboard prompts for typed confirmation.

    Tier requirement

    Sub-projects require the Team tier or higher. The dashboard shows an upgrade prompt for callers below tier; the API returns HTTP 402 with the structured upgrade_url payload.