Skip to main content

Multi-Tenant Authentication

Cargoman Cloud uses a two-layer security model that combines tenant isolation with token-based authentication.

Security Layers

LayerPurposeMechanism
Tenant IsolationDetermines which tenant's data to accessSubdomain in URL
AuthenticationVerifies the caller's identity and permissionsBearer token in header

Both layers are required for any API request. A valid token without tenant context (or vice versa) will be rejected.

How It Works

┌─────────────────────────────────────────────────────────────────┐
│ Request │
│ GET https://acme.packages.cargoman.io/graphql │
│ Host: acme.packages.cargoman.io │
│ Authorization: Bearer <token> │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 1. Tenant Middleware │
│ - Extracts subdomain "acme" from Host header │
│ - Looks up tenant in database │
│ - Injects TenantContext for downstream queries │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 2. Auth Middleware │
│ - Validates Bearer token │
│ - Checks token permissions │
│ - Rejects if invalid (401 Unauthorized) │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 3. Handler │
│ - Executes query scoped to tenant │
│ - Returns only that tenant's data │
└─────────────────────────────────────────────────────────────────┘

Tenant URL Structure

Each tenant gets a unique subdomain:

https://{subdomain}.packages.cargoman.io
└────┬────┘
└── Tenant identifier (e.g., "acme", "beta-corp")

Examples:

  • https://acme.packages.cargoman.io - Acme Corp's registry
  • https://beta-corp.packages.cargoman.io - Beta Corp's registry

REST API Examples

List Packages (Tenant-Scoped)

# Request packages for the "acme" tenant
curl -X GET "https://acme.packages.cargoman.io/api/v1/packages" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json"

Response contains only Acme's packages:

{
"data": [
{
"id": "pkg_abc123",
"name": "acme/payment-gateway",
"repository_url": "https://github.com/acme/payment-gateway"
}
],
"pagination": {
"page": 1,
"total": 1
}
}

Create a Customer

curl -X POST "https://acme.packages.cargoman.io/api/v1/customers" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@client.com",
"name": "Client Developer"
}'

Get Package Details

curl -X GET "https://acme.packages.cargoman.io/api/v1/packages/acme/payment-gateway" \
-H "Authorization: Bearer $ADMIN_TOKEN"

GraphQL API Examples

Query Packages

curl -X POST "https://acme.packages.cargoman.io/graphql" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query { packages(first: 10) { edges { node { id name repositoryUrl } } totalCount } }"
}'

Response:

{
"data": {
"packages": {
"edges": [
{
"node": {
"id": "pkg_abc123",
"name": "acme/payment-gateway",
"repositoryUrl": "https://github.com/acme/payment-gateway"
}
}
],
"totalCount": 1
}
}
}

Create Package Mutation

curl -X POST "https://acme.packages.cargoman.io/graphql" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation CreatePackage($input: CreatePackageInput!) { createPackage(input: $input) { id name } }",
"variables": {
"input": {
"name": "acme/new-module",
"repositoryUrl": "https://github.com/acme/new-module",
"repositoryType": "git"
}
}
}'

Query Customers with Filtering

curl -X POST "https://acme.packages.cargoman.io/graphql" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query($filter: CustomerFilter) { customers(first: 20, filter: $filter) { edges { node { id email status } } } }",
"variables": {
"filter": {
"status": "ACTIVE"
}
}
}'

Error Responses

Missing or Invalid Token (401)

{
"error": "unauthorized",
"message": "Invalid or missing authentication token"
}

Tenant Not Found (404)

{
"error": "tenant_not_found",
"message": "Tenant 'unknown' not found"
}

Tenant Suspended (403)

{
"error": "tenant_suspended",
"message": "Tenant 'acme' is suspended"
}

Security Summary

ScenarioSubdomainTokenResult
Valid requestacme.packages...ValidSuccess - returns Acme's data
No subdomainpackages...ValidError - no tenant context
Wrong subdomainother.packages...ValidError - tenant not found (or other's data)
No tokenacme.packages...Missing401 Unauthorized
Invalid tokenacme.packages...Invalid401 Unauthorized

Composer Protocol

For Composer clients, authentication uses HTTP Basic Auth with the customer token:

# Configure Composer with customer credentials
composer config http-basic.acme.packages.cargoman.io token $CUSTOMER_TOKEN

The subdomain in the repository URL ensures tenant isolation:

{
"repositories": [
{
"type": "composer",
"url": "https://acme.packages.cargoman.io"
}
]
}