Documentation Index Fetch the complete documentation index at: https://mintlify.com/Avelero/avelero/llms.txt
Use this file to discover all available pages before exploring further.
Avelero’s API uses Supabase authentication for user-facing endpoints and API keys for server-to-server communication. All private endpoints require a valid JWT token in the Authorization header.
Authentication methods
JWT tokens (primary)
API keys (internal)
User authentication via Supabase JWT tokens. Use this for all client-facing API calls from the web application.
Server-to-server authentication for internal endpoints. Used by background jobs (Trigger.dev) to communicate with the API.
JWT token authentication
How it works
When a user signs in through Supabase, the client receives a JWT access token. This token must be included in the Authorization header for all authenticated requests.
The API validates tokens and extracts user context in every request:
apps/api/src/trpc/init.ts:113
export async function createTRPCContextFromHeaders (
headers : Record < string , string | undefined >,
) : Promise < TRPCContext > {
const authHeader = headers . authorization ?? headers . Authorization ;
const supabase = createSupabaseForRequest ( authHeader ?? null );
// Extract bearer token and resolve user
const bearerToken = authHeader
? authHeader . startsWith ( "Bearer " )
? authHeader . slice ( "Bearer " . length )
: authHeader
: undefined ;
let user : User | null = null ;
if ( bearerToken ) {
const { data : userRes } = await supabase . auth . getUser ( bearerToken );
user = userRes ?. user ?? null ;
}
// Query user's active brand
let brandId : string | null | undefined = undefined ;
if ( user ) {
const { data : userRow } = await supabase
. from ( "users" )
. select ( "brand_id" )
. eq ( "id" , user . id )
. maybeSingle ();
brandId = userRow ?. brand_id ?? null ;
}
return {
supabase ,
user ,
brandId ,
// ... other context
};
}
Creating a Supabase client
The API creates tenant-specific Supabase clients bound to each request’s auth token:
apps/api/src/trpc/init.ts:71
function createSupabaseForRequest (
authHeader ?: string | null ,
) : SupabaseClient < SupabaseDatabase > {
const url = process . env . NEXT_PUBLIC_SUPABASE_URL as string ;
const anon = process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY as string ;
return createSupabaseJsClient < SupabaseDatabase >( url , anon , {
global: authHeader
? {
headers: {
Authorization: authHeader . startsWith ( "Bearer " )
? authHeader
: `Bearer ${ authHeader } ` ,
},
}
: undefined ,
});
}
Client-side authentication
The tRPC client automatically attaches JWT tokens from the active Supabase session:
apps/app/src/trpc/client.tsx:52
async headers () {
const supabase = createSupabaseClient ();
const {
data : { session },
} = await supabase . auth . getSession ();
const token = session ?. access_token ;
return token
? { Authorization: `Bearer ${ token } ` }
: {};
}
Server-side authentication
Server components use a custom fetch function that includes auth headers from Next.js request context:
apps/app/src/trpc/server.tsx:30
async function fetchWithAuth (
input : RequestInfo | URL ,
init ?: RequestInit ,
) : Promise < Response > {
const token = await getAuthToken ();
const authHeaders : HeadersInit = token
? { Authorization: `Bearer ${ token } ` }
: {};
return fetch ( input , {
... init ,
headers: {
... init ?. headers ,
... authHeaders ,
},
});
}
Brand membership and roles
Brand context resolution
After user authentication, the API resolves the user’s active brand and role:
apps/api/src/trpc/middleware/auth/brand.ts:25
export async function ensureBrandContext (
ctx : TRPCContext ,
) : Promise < BrandContextCache > {
if ( ! ctx . user ) {
return { brandId: null , role: null };
}
const brandId = ctx . brandId ?? null ;
if ( ! brandId ) {
return { brandId: null , role: null };
}
// Query brand membership and role
const membership = await ctx . db . query . brandMembers . findFirst ({
columns: { id: true , role: true },
where : ( brandMembers , { eq , and }) =>
and (
eq ( brandMembers . brandId , brandId ),
eq ( brandMembers . userId , ctx . user ! . id ),
),
});
if ( ! membership ) {
return { brandId: null , role: null };
}
const role = isRole ( membership . role ) ? membership . role : null ;
return { brandId , role };
}
User roles
Avelero supports two user roles within a brand:
Owner - Full access to brand settings, member management, and destructive operations
Member - Standard access to products, catalog, and integrations
See apps/api/src/config/roles.ts:6 for role definitions.
Brand context is cached per request to avoid duplicate database queries. The cache is stored as a symbol on the tRPC context object.
Procedure types and permissions
Public procedures
No authentication required. Used for health checks and public DPP access:
apps/api/src/trpc/init.ts:252
export const publicProcedure = t . procedure ;
Example: Fetching a published DPP by UPID
apps/api/src/trpc/routers/dpp-public/index.ts:97
getByPassportUpid : publicProcedure
. input ( z . object ({ upid: upidSchema }))
. query ( async ({ ctx , input }) => {
const result = await getPublicDppByUpid ( ctx . db , input . upid );
return result ;
})
Protected procedures
Require valid user authentication and brand membership:
apps/api/src/trpc/init.ts:257
export const protectedProcedure = t . procedure
. use ( withBrandContext )
. use ( async ( opts ) => {
const { user , brandId , role } = opts . ctx ;
if ( ! user ) throw unauthorized ();
const authedCtx : AuthenticatedTRPCContext = {
... opts . ctx ,
user: user as User ,
brandId: brandId ?? null ,
role: role ?? null ,
};
return opts . next ({ ctx: authedCtx });
});
Example: Listing products in the current brand
list : protectedProcedure
. input ( z . object ({ limit: z . number (). optional () }))
. query ( async ({ ctx , input }) => {
// ctx.user is guaranteed non-null
// ctx.brandId may be null if user hasn't selected a brand
const products = await ctx . db . query . products . findMany ({
where: eq ( products . brandId , ctx . brandId ),
limit: input . limit ,
});
return products ;
})
Brand-required procedures
Require an active brand selection. Used for mutations that modify brand data:
apps/api/src/trpc/init.ts:285
export const brandRequiredProcedure = protectedProcedure . use ( requireBrand );
const requireBrand = t . middleware (({ ctx , next }) => {
const authedCtx = ctx as AuthenticatedTRPCContext ;
if ( ! authedCtx . brandId ) {
throw noBrandSelected ();
}
const brandId = authedCtx . brandId as string ;
return next ({
ctx: {
... authedCtx ,
brandId ,
},
});
});
Example: Creating a new product
create : brandRequiredProcedure
. input ( productSchema )
. mutation ( async ({ ctx , input }) => {
// ctx.brandId is guaranteed non-null
const product = await ctx . db . insert ( products ). values ({
... input ,
brandId: ctx . brandId ,
});
return product ;
})
When no brand is selected, the API returns a BAD_REQUEST error prompting the user to select or create a brand before proceeding.
API key authentication
Internal API keys
The internal router uses API key authentication for server-to-server communication. Background jobs (Trigger.dev) use this to emit WebSocket progress updates.
apps/api/src/trpc/routers/internal/index.ts:13
const INTERNAL_API_KEY = process . env . INTERNAL_API_KEY ;
if ( ! INTERNAL_API_KEY ) {
throw new Error (
"INTERNAL_API_KEY environment variable must be set"
);
}
Validating API keys
Internal procedures validate the API key in the request input:
apps/api/src/trpc/routers/internal/index.ts:52
emitProgress : publicProcedure
. input ( z . object ({
apiKey: z . string (),
jobId: z . string (),
status: z . enum ([ "PENDING" , "VALIDATING" , "COMPLETED" , "FAILED" ]),
// ... other fields
}))
. mutation ( async ({ input }) => {
// Verify internal API key
if ( input . apiKey !== INTERNAL_API_KEY ) {
throw badRequest ( "Invalid internal API key" );
}
const { apiKey , ... progressData } = input ;
websocketManager . emit ( input . jobId , progressData );
return { success: true };
})
Setting up API keys
Configure the internal API key in your environment:
# apps/api/.env.example
INTERNAL_API_KEY = dev-internal-key
Never commit API keys to version control. Use environment variables and secure secret management in production.
Elevated privileges (admin client)
The API creates an elevated Supabase client with service role permissions for privileged operations:
apps/api/src/trpc/init.ts:97
function createSupabaseAdmin () : SupabaseClient < SupabaseDatabase > | null {
const url = process . env . NEXT_PUBLIC_SUPABASE_URL as string | undefined ;
const serviceKey = process . env . SUPABASE_SERVICE_KEY as string | undefined ;
if ( ! url || ! serviceKey ) return null ;
return createSupabaseJsClient < SupabaseDatabase >( url , serviceKey );
}
The admin client is available in the tRPC context as ctx.supabaseAdmin and is used for:
Deleting storage objects across brands
Removing users from the auth system
Bypassing Row Level Security (RLS) policies
Use the admin client sparingly. Most operations should use the tenant-scoped client (ctx.supabase) to respect RLS policies.
The API enforces security headers on all responses:
app . use ( secureHeaders ());
This includes:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Error responses
Authentication errors use standardized tRPC error codes:
Unauthorized (missing auth)
{
"error" : {
"code" : "UNAUTHORIZED" ,
"message" : "Authentication required"
}
}
Forbidden (insufficient permissions)
{
"error" : {
"code" : "FORBIDDEN" ,
"message" : "Insufficient permissions"
}
}
Bad request (no brand selected)
{
"error" : {
"code" : "BAD_REQUEST" ,
"message" : "No brand selected. Please select or create a brand."
}
}
Best practices
Always use HTTPS in production
Never send JWT tokens over unencrypted connections. Configure SSL/TLS certificates for your API domain.
Implement token refresh
Supabase tokens expire after 1 hour. Implement automatic refresh in your client to prevent session interruptions.
Handle brand selection
Check for BAD_REQUEST errors indicating missing brand context. Prompt users to select a brand before making mutations.
Use request-scoped clients
Always use ctx.supabase (not a global client) to ensure queries respect the authenticated user’s permissions.
Validate roles on the server
Never trust client-side role checks. Always validate permissions in API procedures using ctx.role.
Next steps
Introduction Learn about the API architecture and design
Products API Explore the products API endpoints