Skip to Content
ConceptsOrganizations and Brands

Organizations and Brands

Three nouns organize everything Synq does. Once they click, the rest of the docs reads like a single document instead of a dozen.

The picture

Organization (acme-co) ├─ Brand (marketplace) │ ├─ Connection (google: BYO) │ ├─ Connection (apple: BYO) │ ├─ Scope (wallets:read) │ │ │ ├─ App (marketplace-web) — web, confidential │ ├─ App (marketplace-mobile) — native, public │ └─ App (marketplace-cli) — agent, device flow └─ Brand (admin-tools) ├─ Connection (microsoft: BYO) └─ App (admin-web) — web, confidential

One Org. Two Brands (two distinct trust surfaces). Each brand has its own BYO OAuth credentials, its own consent screen, and its own set of Apps.

Organization — the developer entity

Holds the billing relationship, the membership and roles, the team API keys, the audit log, the webhook subscriptions, and the token gates.

interface Org { id: string // opaque, immutable name: string // human-readable, non-unique slug: string // URL-safe, [a-z0-9-]+, globally unique description?: string iconFile?: FileRef createdBy: string // user id of the creator, immutable stripeProductId?: string | null // current subscription (null = free tier) stripeCustomerId?: string createdAt: string updatedAt: string }

You invite teammates to the Org. You assign them roles (built-in: Owner, Admin, Member; custom roles are supported). Roles map to permissions; see Permissions.

Brand — the trust surface

The thing the end user actually sees, trusts, and consents to.

interface Brand { id: string org: string // owning Organization id slug: string // unique within the org name: string // shown on Synq consent screen oauthCallbackId: string // globally unique, immutable logoFile?: FileRef // shown on consent + sign-in surface primaryColor?: string // e.g. '#5FD8CA' supportEmail?: string privacyPolicyUrl?: string termsUrl?: string providerOrder: string[] // default sign-in modal order createdAt: string updatedAt: string }

A Brand owns:

  • The sign-in surface — theming, copy, layout. End users see this brand on the consent screen, not Synq.
  • The OAuth provider credentials — BYO Google, Apple, Discord, etc. Stored encrypted at rest. See BYO credentials.
  • The scope vocabulary — beyond OIDC standard scopes, you can declare brand-specific scopes (e.g. wallets:read, marketplace:write). Apps under the brand can request any subset of these.
  • The consent surface — the screen the user sees the first time an app asks for scopes.

Why “oauthCallbackId” exists

When a user is bounced from your app to Google to sign in, Google returns them to a fixed redirect URI you registered with Google. But Synq supports thousands of brands, each with their own Google credentials. How does Synq know which brand a callback belongs to?

Answer: the redirect URI contains the oauthCallbackId of the brand:

https://synq.id/oauth/google/<oauthCallbackId>/callback

You register that URL with Google when you set up BYO. The oauthCallbackId is globally unique, immutable, and opaque. The brand can change name, slug, color, logo, anything — the callback URL stays stable, and Google never needs to be reconfigured.

If a single brand uses Synq’s default Google credentials (not BYO), the callback URL is https://synq.id/oauth/google/callback (no brand id needed because there is only one default app on Google’s side).

App — the OIDC client

An App is what your code identifies as. A web app, a backend, a mobile app, an agent, a CLI: each is one App. Multi-platform products have one App per platform under the same Brand.

interface App { id: string brand: string org: string clientId: string // public, sent on every OAuth request name: string // human-readable type: 'web' | 'spa' | 'native' | 'm2m' | 'agent' isConfidential: boolean // has a client_secret redirectUris: string[] // exact match required allowedGrantTypes: GrantType[] allowedScopes: string[] enabled: boolean description?: string }

App types and their grants

App type determines which grants Synq will issue tokens through. Pick the type up-front; it constrains the auth flows the App can use.

TypeConfidential?Default grantsUse it for
webyes (client_secret)authorization_code + refresh_tokenNext.js, Rails, Django, Express. Server-rendered or with a backend you control.
spano (PKCE only)authorization_code + refresh_tokenVite/Create-React-App, Vue, Svelte, anything fully browser-side.
nativeno (PKCE only)authorization_code + refresh_tokenMobile (Expo, RN, native iOS/Android).
m2myesclient_credentialsService-to-service. No user, no consent, no refresh token.
agentconfigurabledevice_code (+ refresh_token)CLIs and AI agents that cannot embed a browser.

A common pattern is to have multiple Apps under one Brand: a web App for your dashboard, a native App for your mobile, an m2m App for your background workers. They all share the brand’s trust surface; they just have different client_ids.

Public vs confidential clients

A confidential client (isConfidential: true) has a client_secret that lives only on a backend you control. The secret is sent on every token-endpoint call to prove the App is who it claims to be.

A public client cannot hold a secret (SPAs, mobile, CLIs all ship to user-controlled environments). Public clients are protected instead by PKCE — Proof Key for Code Exchange. The flow:

  1. Client generates code_verifier (random) and code_challenge = SHA256(code_verifier).
  2. Client sends code_challenge + code_challenge_method=S256 to /oauth2/authorize.
  3. Client sends code_verifier to /oauth2/token along with the auth code.
  4. Synq SHA256s the verifier and checks it matches the challenge from step 2.

Synq requires PKCE for public clients. Confidential clients should use it too (defense in depth) but it is optional there.

When you need multiple brands

A simple rule:

You need a second Brand when you have a second trust surface.

Multi-brand examples:

  • Multiple consumer products under one company. E.g. you run Burn & Claim and Spotlight. They have different logos, different privacy policies, different users. One Brand each.
  • White-label or reseller platform. Each tenant gets their own Brand — own logo, own BYO credentials, own consent screen.
  • Distinct staff-facing surface. Your customer-facing app and your internal admin tool can share an Org but live under different Brands if their look and feel diverges enough.

You do not need a second Brand for:

  • Multiple platforms of the same product (web + mobile + CLI under the same brand → one Brand, three Apps).
  • Different environments (dev, staging, prod → one Brand; per-environment Apps if you want full isolation, or per-environment redirect URIs on the same App).

You can move Apps between Brands later if you change your mind, so don’t over-think it on day one. Start with one Brand and split when there is a real second trust surface to defend.

Multi-tenancy patterns

Synq is multi-tenant at the Brand layer. Common shapes:

Single-tenant product — one Org, one Brand, N Apps. Everything you do is under the same Brand.

Multi-product company — one Org, M Brands (one per product), N Apps per Brand. Same billing relationship, separate trust surfaces.

Platform / reseller — one Org per customer (so they own their billing), one Brand each, BYO credentials per Brand. Or one Org for your company with M Brands (one per customer) if you handle billing for them.

Enterprise SSO federation — one Org per enterprise customer, one Brand per customer, BYO Microsoft / SAML connection on each Brand. Higher tier; see Pricing .

The lifecycle of a request

When a user signs in, the actors flow like this:

1. User clicks "Sign in" in App (browser at app.example.com) 2. App redirects browser to Synq /oauth2/authorize?client_id=…&redirect_uri=…&state=… 3. Synq fetches the App, finds owning Brand, sets brand context 4. Synq shows sign-in surface (themed via the Brand: logo, color, copy) 5. User picks a provider, e.g. Google 6. Synq builds OAuth request to Google - Uses Brand's BYO Google client_id+secret if configured - Else Synq's default Google credentials - redirect_uri = https://synq.id/oauth/google/<brand.oauthCallbackId>/callback 7. User completes Google login, Google calls back to Synq 8. Synq receives the OAuth tokens from Google, looks up or creates the Synq user, generates an auth code 9. Synq redirects browser back to App /api/oauth/callback?code=…&state=… 10. App exchanges code for tokens at /oauth2/token 11. App has access_token + id_token + refresh_token. Done.

The Brand context — set at step 3 — flows through everything: the sign-in surface in step 4, the BYO credential choice in step 6, the consent surface, the audit log entries, the issued access token’s aud claim. The Brand is the lens through which Synq sees the whole transaction.

Mental model summary

  • Org = developer entity. Billing, members, roles, team API keys, audit log, webhooks, token gates. Your team works here.
  • Brand = trust surface. Theming, BYO credentials, consent screen, scope vocabulary. The end user sees this.
  • App = OIDC client. client_id, redirect URIs, allowed grants and scopes. Your code identifies as this.

In code, the three nouns rarely appear together — once you have the client_id and client_secret for your App, you don’t think about the parent Brand or Org. They are the mental scaffolding that makes the rest of the system make sense.

Last updated on