{
  "version": "https://jsonfeed.org/version/1",
  "title": "auth0 on benrants",
  "icon": "https://www.gravatar.com/avatar/d8008c01f3bd4371cc66a4a8ee29f55f?s=96&d=https%3A%2F%2Fmicro.blog%2Fimages%2Fblank_avatar.png",
  "home_page_url": "https://benrants.com/",
  "feed_url": "https://benrants.com/feed.json",
  "items": [
      {
        "id": "http://benrants.micro.blog/2026/06/19/auth-organisations-self-service-signup.html",
        "title": "Auth0 organisations self service signup",
        "content_html": "<p>Auth0 is a SaaS identity provider that lets you add production-grade authentication to your product quickly. It offers two main flavours: Business-to-Consumer (B2C) and Business-to-Business (B2B).</p>\n<p>Today, we’re focusing on the B2B use case.</p>\n<p>Auth0 B2B revolves around <strong>Organizations</strong>: a model you can use to represent your own tenants, companies, or organisations. You can map users to one or more organisations, assign them different roles inside each one, and enable SSO connections, complete with domain discovery, on a per-organisation basis.</p>\n<p>An Auth0 application can be configured for one of three login experiences:</p>\n<ul>\n<li><strong>Individual users</strong> — this is for applications like Netflix, YouTube, or Facebook.</li>\n<li><strong>Business users</strong> — this is for B2B SaaS applications where every user will be part of one or more tenants/organisations.</li>\n<li><strong>Both</strong> — this is for combinations of the above two, for applications like GitHub or Figma, where a user might have a personal account and resources but also be part of an organisation with organisational resources.</li>\n</ul>\n<p>If you’re building a B2B product with SSO or multi-tenant requirements, you’ll want to use Organizations together with the <strong>Business users</strong> login flow. This combination handles most of the hard work for you, letting Auth0 handle invitations, self-service SSO configuration, and providing org selection after login for multi-tenant systems, but it comes with a catch: when an application is set to Business users, a brand-new customer can sign up and then immediately be unable to log in, because they don’t yet belong to any organisation.</p>\n<p>There are two common ways around this.</p>\n<p>The first is to let new users create an organisation before they authenticate, then redirect them to sign up against that organisation. This has a couple of serious drawbacks. First, it requires an unauthenticated endpoint that lets anyone create organisations, which an attacker could use to flood both your database and your Auth0 organisation-usage limits. Second, if the user abandons the flow, or if anything goes wrong between creating the organisation and linking the user account, you end up with ghost organisations and frustrated users who have to start over.</p>\n<p>The second approach, which we&rsquo;ll cover today, is to have the user create their Auth0 account first, sign in, and then enter an onboarding flow where they provide their personal details and create their organisation. This avoids the pitfalls above, but it isn’t the immediately obvious solution, and Auth0 doesn’t document it in one place. Let’s look at how to build it.</p>\n<hr>\n<h2 id=\"so-what-are-we-going-to-do\">So what are we going to do?</h2>\n<p>The solution to the problem is actually quite simple once you realise all it takes is three Auth0 applications, a bit of redirection through your frontend based on the user’s state, and some Auth0 Management API calls from your backend.</p>\n<p><strong>&ldquo;Why three Auth0 applications?&quot;</strong> I hear you say. Well, we need two applications so one can act as an &ldquo;individual&rdquo; login application, allowing a user without an organisation to log in, whilst the other acts as the primary application, running the &ldquo;business users&rdquo; login flow so we get access to the org selection screen if needed, and the automatic attaching of the <code>org_id</code> claim on the user’s access token. Lastly, we need an M2M application set up to access the Management API so we can create the user’s organisation and attach them to it.</p>\n<p><strong>&ldquo;Why can’t we just use the &lsquo;Both&rsquo; login flow?&quot;</strong> You can! And if you do, it makes life simpler; however, you end up with either an annoying &ldquo;Personal login&rdquo; button under the login screen, or you end up with users always needing to select either &ldquo;Personal account&rdquo; or &ldquo;Organisation 1&rdquo; when they log in, which is just a bit shit for a B2B application, isn’t it. If you are happy with that compromise, or your application works with both types of users, then go for it!</p>\n<h3 id=\"auth0-setup\">Auth0 setup</h3>\n<p>So to start, let’s set up our Auth0 applications. I will be using the Auth0 Dashboard to do all the Auth0 setup, but everything can be replicated using the Deploy CLI if you are already using it or if you want to start using it. I will also be working with a TanStack Start application, but again, everything should be replicable for any other frontend setup.</p>\n<p>First, let’s set up our <strong>onboarding</strong> application. Create a new application in Auth0 — for our example I will be creating a regular web application. Once created, set the allowed callback URL to <code>http://localhost:5173/auth/callback/onboarding</code>. The default login flow is for <strong>Individuals</strong>, but you should validate this by checking the <strong>Login Experience</strong> tab. You don’t need to set up anything else specifically for this setup, so feel free to add any other config you need, like access to specific APIs or specific allowed origins for CORS.</p>\n<p>Next, we will set up another application, <strong>main</strong>, which we will configure a bit more. Again, I will be creating a regular web application, and we will add <code>http://localhost:5173/auth/callback/main</code> to the allowed callback URLs. We will also want to change the <strong>Login Experience</strong> to <strong>Business users</strong> with the login flow of <strong>Prompt for credentials</strong>.</p>\n<img src=\"https://eu.uploads.micro.blog/283069/2026/image.png\" width=\"600\" height=\"672\" alt=\"Auth0 Login Experience settings showing Individuals, Business Users and Both options with the Business Users option selected.\">\n<p>Again, you can set up any other configuration you need to, like API access.</p>\n<p>Lastly, let’s set up the machine-to-machine client so we can call the Management API. Create a new M2M application — I named mine <strong>Management M2M</strong>. Once it’s created, navigate to <strong>API access</strong> and open the Management API permissions setup. We are going to give this client the following scopes under <strong>Client access</strong>:</p>\n<ul>\n<li>Users: <code>read:users</code>, <code>update:users</code></li>\n<li>Organizations: <code>create:organizations</code>, <code>update:organizations</code></li>\n<li>Organization members: <code>create:organization_members</code></li>\n<li>Organization connections: <code>create:organization_connections</code>, <code>read:organization_connections</code>, <code>update:organization_connections</code></li>\n<li>Organization member roles: <code>create:organization_member_roles</code></li>\n</ul>\n<p>Once you’ve set these up, hit save!</p>\n<img src=\"https://eu.uploads.micro.blog/283069/2026/f0e6f5c50a.png\" width=\"550\" height=\"743\" alt=\"Auth0 Management API permissions screen showing selected scopes for Users, Organizations, and related resources.\">\n<p>We will also want to set up some roles in Auth0 that we will assign to users during signup. To do this, navigate to the <strong>Roles</strong> section under <strong>User management</strong>. We will add two roles today, <strong>administrator</strong> and <strong>member</strong>. Make a note of both role IDs — we will use these later.</p>\n<p>Now that Auth0 is set up, let’s move on to the frontend and backend setup.</p>\n<hr>\n<h2 id=\"into-the-code\">Into the code</h2>\n<h3 id=\"set-up-the-api\">Set up the API</h3>\n<p>Let’s go over the API endpoints we will be creating for onboarding. I’m going to keep these simple, focusing on Auth0 calls more than anything else. They are also written in C#, but the concepts are the same.</p>\n<p>First, the environment or configuration variables you will use to interact with Auth0:</p>\n<pre tabindex=\"0\"><code>Auth0__Domain=auth0 tenant domain or custom domain if you have one\nAuth0__Audience=audience  # only if you have set one up\nAuth0__ManagementClient__ClientId=m2m client id\nAuth0__ManagementClient__ClientSecret=m2m client secret\nAuth0__ManagementClient__Domain=auth0 tenant domain\nAuth0__AppClient__ClientId=main client id\nAuth0__AppClient__RedirectUri=&quot;http://localhost:5173/auth/callback/main&quot;\nAuth0__DbConnection__ConnectionId=auth0 database connection id\nAuth0__DbConnection__ConnectionName=&quot;Username-Password-Authentication&quot;\nAuth0__Roles__Admin=administrator role id\nAuth0__Roles__Member=member role id\n</code></pre><p>You can find your database connection ID by going to <strong>Authentication → Database → Username-Password-Authentication</strong> in the Auth0 Dashboard.</p>\n<p>Now let’s create a <code>POST /users</code> endpoint that will be used to create the user record in our database and update the user details in Auth0 so their name is available on the ID token.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-csharp\" data-lang=\"csharp\"><span style=\"color:#a6e22e\">[HttpPost]</span>\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">async</span> Task&lt;ActionResult&lt;Guid&gt;&gt; CreateUser(\n<span style=\"color:#a6e22e\">    [FromBody]</span> PostUserResourceRepresentation request,\n    CancellationToken cancellationToken)\n{\n    <span style=\"color:#66d9ef\">var</span> auth0UserId = User.GetRequiredUserId();\n    <span style=\"color:#75715e\">// get the &#34;sub&#34; claim from the access token (Auth0 id)\n</span><span style=\"color:#75715e\"></span>\n    <span style=\"color:#66d9ef\">var</span> auth0User = <span style=\"color:#66d9ef\">await</span> auth0.Users.GetAsync(\n        auth0UserId,\n        <span style=\"color:#66d9ef\">new</span> GetUserRequestParameters(),\n        <span style=\"color:#66d9ef\">null</span>,\n        cancellationToken);\n\n    <span style=\"color:#66d9ef\">if</span> (<span style=\"color:#66d9ef\">string</span>.IsNullOrEmpty(auth0User?.Email))\n    {\n        <span style=\"color:#75715e\">// ... error response as something&#39;s gone wrong at Auth0 level\n</span><span style=\"color:#75715e\"></span>    }\n\n    <span style=\"color:#66d9ef\">var</span> existingUser = <span style=\"color:#66d9ef\">await</span> userRepository.GetUserByAuth0IdAsync(auth0UserId, cancellationToken);\n    <span style=\"color:#66d9ef\">if</span> (existingUser <span style=\"color:#66d9ef\">is</span> not <span style=\"color:#66d9ef\">null</span>)\n    {\n        <span style=\"color:#75715e\">// ... respond with conflict/bad request etc.\n</span><span style=\"color:#75715e\"></span>    }\n\n    <span style=\"color:#66d9ef\">var</span> user = UserBuilder.From(request, auth0User.Email, auth0UserId);\n\n    <span style=\"color:#66d9ef\">await</span> userRepository.CreateUserAsync(user, cancellationToken); <span style=\"color:#75715e\">// create user in your db\n</span><span style=\"color:#75715e\"></span>\n    <span style=\"color:#66d9ef\">await</span> auth0.Users.UpdateAsync(\n        auth0UserId,\n        <span style=\"color:#66d9ef\">new</span> UpdateUserRequestContent\n        {\n            GivenName = request.FirstName,\n            FamilyName = request.LastName,\n            Name = <span style=\"color:#e6db74\">$&#34;{request.FirstName} {request.LastName}&#34;</span>\n        },\n        cancellationToken: cancellationToken); <span style=\"color:#75715e\">// update user details in Auth0 for the ID token\n</span><span style=\"color:#75715e\"></span>\n    <span style=\"color:#66d9ef\">return</span> CreatedAtAction(nameof(GetById), <span style=\"color:#66d9ef\">new</span> { id = user.Id }, user.Id);\n}\n</code></pre></div><p>The user endpoint is pretty simple; nothing to note here.</p>\n<p>Next, let’s add the <code>POST /organisations</code> endpoint we will use to create the new organisation and link the user to it.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-csharp\" data-lang=\"csharp\"><span style=\"color:#a6e22e\">[HttpPost]</span>\n<span style=\"color:#a6e22e\">[Authorize]</span>\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">async</span> Task&lt;ActionResult&lt;OrganisationResponse&gt;&gt; CreateOrganisation(\n<span style=\"color:#a6e22e\">    [FromBody]</span> CreateOrganisationRequest request)\n{\n    <span style=\"color:#66d9ef\">var</span> auth0UserId = User.FindFirst(<span style=\"color:#e6db74\">&#34;sub&#34;</span>)?.Value;\n    <span style=\"color:#66d9ef\">if</span> (auth0UserId <span style=\"color:#66d9ef\">is</span> <span style=\"color:#66d9ef\">null</span>)\n        <span style=\"color:#66d9ef\">return</span> Unauthorized();\n\n    <span style=\"color:#75715e\">// 1. Persist the organisation locally\n</span><span style=\"color:#75715e\"></span>    <span style=\"color:#66d9ef\">var</span> org = <span style=\"color:#66d9ef\">new</span> Organisation\n    {\n        Id = Guid.NewGuid(),\n        Name = request.Name,\n        CreatedAt = DateTime.UtcNow\n    };\n\n    <span style=\"color:#66d9ef\">await</span> <span style=\"color:#ae81ff\">_</span>orgs.AddAsync(org); <span style=\"color:#75715e\">// save to db\n</span><span style=\"color:#75715e\"></span>\n    <span style=\"color:#75715e\">// 2. Create the matching Auth0 organisation\n</span><span style=\"color:#75715e\"></span>    <span style=\"color:#66d9ef\">var</span> auth0Org = <span style=\"color:#66d9ef\">await</span> <span style=\"color:#ae81ff\">_</span>auth0.Organizations.CreateAsync(<span style=\"color:#66d9ef\">new</span>()\n    {\n        Name = <span style=\"color:#e6db74\">$&#34;org-{org.Id:N}&#34;</span>,\n        DisplayName = request.Name\n    });\n\n    org.Auth0OrgId = auth0Org.Id;\n    <span style=\"color:#66d9ef\">await</span> <span style=\"color:#ae81ff\">_</span>orgs.UpdateAsync(org); <span style=\"color:#75715e\">// map Auth0 id to db record\n</span><span style=\"color:#75715e\"></span>\n    <span style=\"color:#75715e\">// 3. Add the calling user as a member and make them admin\n</span><span style=\"color:#75715e\"></span>    <span style=\"color:#66d9ef\">await</span> <span style=\"color:#ae81ff\">_</span>auth0.Organizations.Members.CreateAsync(auth0Org.Id, <span style=\"color:#66d9ef\">new</span>()\n    {\n        Members = [auth0UserId]\n    }); <span style=\"color:#75715e\">// add the current user to the Auth0 org\n</span><span style=\"color:#75715e\"></span>\n    <span style=\"color:#66d9ef\">var</span> adminRoleId = <span style=\"color:#ae81ff\">_</span>config[<span style=\"color:#e6db74\">&#34;Auth0:Roles:Admin&#34;</span>];\n\n    <span style=\"color:#66d9ef\">await</span> <span style=\"color:#ae81ff\">_</span>auth0.Organizations.Members.Roles.AssignAsync(\n        auth0Org.Id,\n        auth0UserId,\n        <span style=\"color:#66d9ef\">new</span>() { Roles = [adminRoleId] }\n    ); <span style=\"color:#75715e\">// make the user an administrator for this org\n</span><span style=\"color:#75715e\"></span>\n    <span style=\"color:#75715e\">// 4. Record membership in your own database\n</span><span style=\"color:#75715e\"></span>    <span style=\"color:#66d9ef\">var</span> user = <span style=\"color:#66d9ef\">await</span> <span style=\"color:#ae81ff\">_</span>users.GetByAuth0IdAsync(auth0UserId);\n    user.AddOrganisation(org.Id, OrganisationRole.Admin);\n    <span style=\"color:#66d9ef\">await</span> <span style=\"color:#ae81ff\">_</span>users.UpdateAsync(user);\n\n    <span style=\"color:#75715e\">// 5. Return a URL the frontend redirects to so the user logs into the org\n</span><span style=\"color:#75715e\"></span>    <span style=\"color:#66d9ef\">var</span> redirectUrl = BuildAuth0RedirectUrl(auth0Org.Id);\n\n    <span style=\"color:#66d9ef\">return</span> CreatedAtAction(\n        nameof(GetOrganisation),\n        <span style=\"color:#66d9ef\">new</span> { id = org.Id },\n        <span style=\"color:#66d9ef\">new</span> OrganisationResponse(org.Id, org.Name, redirectUrl));\n}\n\n<span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">string</span> BuildAuth0RedirectUrl(<span style=\"color:#66d9ef\">string</span> auth0OrgId)\n{\n    <span style=\"color:#66d9ef\">var</span> domain = <span style=\"color:#ae81ff\">_</span>config[<span style=\"color:#e6db74\">&#34;Auth0:Domain&#34;</span>];\n    <span style=\"color:#66d9ef\">var</span> clientId = <span style=\"color:#ae81ff\">_</span>config[<span style=\"color:#e6db74\">&#34;Auth0:MainApp:ClientId&#34;</span>];\n    <span style=\"color:#66d9ef\">var</span> redirectUri = Uri.EscapeDataString(<span style=\"color:#ae81ff\">_</span>config[<span style=\"color:#e6db74\">&#34;Auth0:MainApp:RedirectUri&#34;</span>]!);\n    <span style=\"color:#66d9ef\">var</span> connection = Uri.EscapeDataString(<span style=\"color:#ae81ff\">_</span>config[<span style=\"color:#e6db74\">&#34;Auth0:DbConnectionName&#34;</span>]!);\n\n    <span style=\"color:#66d9ef\">return</span> <span style=\"color:#e6db74\">&#34;https://&#34;</span> +\n           <span style=\"color:#e6db74\">$&#34;{domain}/authorize&#34;</span> +\n           <span style=\"color:#e6db74\">$&#34;?organization={auth0OrgId}&#34;</span> +\n           <span style=\"color:#e6db74\">$&#34;&amp;connection={connection}&#34;</span> +\n           <span style=\"color:#e6db74\">$&#34;&amp;client_id={clientId}&#34;</span> +\n           <span style=\"color:#e6db74\">$&#34;&amp;redirect_uri={redirectUri}&#34;</span> +\n           <span style=\"color:#e6db74\">&#34;&amp;response_type=code&#34;</span> +\n           <span style=\"color:#e6db74\">$&#34;&amp;scope={Uri.EscapeDataString(&#34;</span>openid profile email offline_access<span style=\"color:#e6db74\">&#34;)}&#34;</span>;\n}\n</code></pre></div><p>This endpoint will return a response like:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-json\" data-lang=\"json\">{\n  <span style=\"color:#f92672\">&#34;id&#34;</span>: <span style=\"color:#e6db74\">&#34;a1b2c3d4-e5f6-7890-abcd-ef1234567890&#34;</span>,\n  <span style=\"color:#f92672\">&#34;name&#34;</span>: <span style=\"color:#e6db74\">&#34;Acme Ltd&#34;</span>,\n  <span style=\"color:#f92672\">&#34;redirect&#34;</span>: <span style=\"color:#e6db74\">&#34;https://your-tenant.us.auth0.com/authorize?organization=org_abc123&amp;connection=Username-Password-Authentication&amp;client_id=MAIN_CLIENT_ID&amp;redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&amp;response_type=code&amp;scope=openid%20profile%20email%20offline_access&#34;</span>\n}\n</code></pre></div><p><strong>&ldquo;Why are we returning a redirect URL?&quot;</strong> We return a redirect URL from the API because we need to get the user to log in to Auth0 against this new organisation. This login will happen against the <strong>main</strong> client, which has the <strong>Business users</strong> flow, and will mean the user’s token has the <code>org_id</code> claim.</p>\n<p>That’s our API endpoints sorted — very simple for user details, fairly simple for organisation details. You can extend these as much as you want. For example, in a production setup we should roll back the organisation database record if the Auth0 organisation can’t be created.</p>\n<p>Let’s move on to the frontend application next.</p>\n<h3 id=\"set-up-the-frontend\">Set up the frontend</h3>\n<p>First, the environment variables we need, like we did with the API:</p>\n<pre tabindex=\"0\"><code># Auth0 credentials - obtain from https://manage.auth0.com\n# Main application (Business Users / organization_usage: require)\nAUTH0_DOMAIN=auth0 tenant domain or custom domain\nAUTH0_CLIENT_ID=main client id\nAUTH0_CLIENT_SECRET=main client secret\nAUTH0_AUDIENCE=audience if set up\n\n# Onboarding Auth0 application (Individuals / organization_usage: deny)\nAUTH0_ONBOARDING_CLIENT_ID=onboarding client id\nAUTH0_ONBOARDING_CLIENT_SECRET=onboarding client secret\n\n# Cookie encryption secret\nAUTH0_SECRET=random value, generate with: openssl rand -hex 32\n</code></pre><p>Next, install the Auth0 package we will be using:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-bash\" data-lang=\"bash\">pnpm install @auth0/auth0-server-js\n</code></pre></div><p>TanStack Start isn’t quite fully released yet, so Auth0 haven’t set up a package specifically for it like they have for Next, so we need to do some of it ourselves.</p>\n<p>Now that our environment variables are set up and we have the Auth0 server package installed, let’s set up the Auth0 clients. Both Auth0 clients share the same session store and cookie encryption secret, but they each have their own client ID, secret, and redirect URI, so we need a factory.</p>\n<p>Create a new file under <code>src/lib/auth.server.ts</code>:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#66d9ef\">import</span> {\n  <span style=\"color:#a6e22e\">CookieTransactionStore</span>,\n  <span style=\"color:#a6e22e\">ServerClient</span>,\n  <span style=\"color:#a6e22e\">StatefulStateStore</span>,\n} <span style=\"color:#66d9ef\">from</span> <span style=\"color:#e6db74\">&#34;@auth0/auth0-server-js&#34;</span>;\n<span style=\"color:#66d9ef\">import</span> { <span style=\"color:#a6e22e\">env</span> } <span style=\"color:#66d9ef\">from</span> <span style=\"color:#e6db74\">&#34;cloudflare:workers&#34;</span>;\n\n<span style=\"color:#66d9ef\">export</span> <span style=\"color:#66d9ef\">type</span> <span style=\"color:#a6e22e\">Auth0Mode</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#e6db74\">&#34;main&#34;</span> <span style=\"color:#f92672\">|</span> <span style=\"color:#e6db74\">&#34;onboarding&#34;</span>;\n\n<span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">auth0Clients</span>: <span style=\"color:#66d9ef\">Record</span>&lt;<span style=\"color:#f92672\">Auth0Mode</span><span style=\"color:#960050;background-color:#1e0010\">,</span> <span style=\"color:#a6e22e\">ServerClient</span><span style=\"color:#960050;background-color:#1e0010\">&lt;</span><span style=\"color:#a6e22e\">void</span>&gt; <span style=\"color:#f92672\">|</span> <span style=\"color:#66d9ef\">undefined</span><span style=\"color:#f92672\">&gt;</span> <span style=\"color:#f92672\">=</span> {\n  <span style=\"color:#a6e22e\">main</span>: <span style=\"color:#66d9ef\">undefined</span>,\n  <span style=\"color:#a6e22e\">onboarding</span>: <span style=\"color:#66d9ef\">undefined</span>,\n};\n\n<span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">secret</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">process</span>.<span style=\"color:#a6e22e\">env</span>[<span style=\"color:#e6db74\">&#34;AUTH0_SECRET&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>;\n<span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">sessionStore</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#66d9ef\">new</span> <span style=\"color:#a6e22e\">KvSessionStore</span>(<span style=\"color:#a6e22e\">env</span>.<span style=\"color:#a6e22e\">AUTH_SESSIONS</span>); <span style=\"color:#75715e\">// or your own store\n</span><span style=\"color:#75715e\"></span>\n<span style=\"color:#66d9ef\">function</span> <span style=\"color:#a6e22e\">getClientConfig</span>(<span style=\"color:#a6e22e\">mode</span>: <span style=\"color:#66d9ef\">Auth0Mode</span>) {\n  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">appUrl</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">process</span>.<span style=\"color:#a6e22e\">env</span>[<span style=\"color:#e6db74\">&#34;APP_URL&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>;\n\n  <span style=\"color:#66d9ef\">if</span> (<span style=\"color:#a6e22e\">mode</span> <span style=\"color:#f92672\">===</span> <span style=\"color:#e6db74\">&#34;main&#34;</span>) {\n    <span style=\"color:#66d9ef\">return</span> {\n      <span style=\"color:#a6e22e\">clientId</span>: <span style=\"color:#66d9ef\">process.env</span>[<span style=\"color:#e6db74\">&#34;AUTH0_CLIENT_ID&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>,\n      <span style=\"color:#a6e22e\">clientSecret</span>: <span style=\"color:#66d9ef\">process.env</span>[<span style=\"color:#e6db74\">&#34;AUTH0_CLIENT_SECRET&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>,\n      <span style=\"color:#a6e22e\">redirectUri</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">`</span><span style=\"color:#e6db74\">${</span><span style=\"color:#a6e22e\">appUrl</span><span style=\"color:#e6db74\">}</span><span style=\"color:#e6db74\">/auth/callback/main`</span>,\n    };\n  }\n\n  <span style=\"color:#66d9ef\">return</span> {\n    <span style=\"color:#a6e22e\">clientId</span>: <span style=\"color:#66d9ef\">process.env</span>[<span style=\"color:#e6db74\">&#34;AUTH0_ONBOARDING_CLIENT_ID&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>,\n    <span style=\"color:#a6e22e\">clientSecret</span>: <span style=\"color:#66d9ef\">process.env</span>[<span style=\"color:#e6db74\">&#34;AUTH0_ONBOARDING_CLIENT_SECRET&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>,\n    <span style=\"color:#a6e22e\">redirectUri</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">`</span><span style=\"color:#e6db74\">${</span><span style=\"color:#a6e22e\">appUrl</span><span style=\"color:#e6db74\">}</span><span style=\"color:#e6db74\">/auth/callback/onboarding`</span>,\n  };\n}\n\n<span style=\"color:#66d9ef\">export</span> <span style=\"color:#66d9ef\">function</span> <span style=\"color:#a6e22e\">getAuth0</span>(<span style=\"color:#a6e22e\">mode</span>: <span style=\"color:#66d9ef\">Auth0Mode</span>)<span style=\"color:#f92672\">:</span> <span style=\"color:#a6e22e\">ServerClient</span>&lt;<span style=\"color:#f92672\">void</span>&gt; {\n  <span style=\"color:#66d9ef\">if</span> (<span style=\"color:#f92672\">!</span><span style=\"color:#a6e22e\">auth0Clients</span>[<span style=\"color:#a6e22e\">mode</span>]) {\n    <span style=\"color:#66d9ef\">const</span> { <span style=\"color:#a6e22e\">clientId</span>, <span style=\"color:#a6e22e\">clientSecret</span>, <span style=\"color:#a6e22e\">redirectUri</span> } <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">getClientConfig</span>(<span style=\"color:#a6e22e\">mode</span>);\n\n    <span style=\"color:#a6e22e\">auth0Clients</span>[<span style=\"color:#a6e22e\">mode</span>] <span style=\"color:#f92672\">=</span> <span style=\"color:#66d9ef\">new</span> <span style=\"color:#a6e22e\">ServerClient</span>&lt;<span style=\"color:#f92672\">void</span>&gt;({\n      <span style=\"color:#a6e22e\">domain</span>: <span style=\"color:#66d9ef\">process.env</span>[<span style=\"color:#e6db74\">&#34;AUTH0_DOMAIN&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>,\n      <span style=\"color:#a6e22e\">clientId</span>,\n      <span style=\"color:#a6e22e\">clientSecret</span>,\n      <span style=\"color:#a6e22e\">authorizationParams</span><span style=\"color:#f92672\">:</span> {\n        <span style=\"color:#a6e22e\">redirect_uri</span>: <span style=\"color:#66d9ef\">redirectUri</span>,\n        <span style=\"color:#a6e22e\">audience</span>: <span style=\"color:#66d9ef\">process.env</span>[<span style=\"color:#e6db74\">&#34;AUTH0_AUDIENCE&#34;</span>] <span style=\"color:#66d9ef\">as</span> <span style=\"color:#66d9ef\">string</span>,\n        <span style=\"color:#a6e22e\">scope</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;openid profile email offline_access&#34;</span>,\n      },\n      <span style=\"color:#75715e\">// These two stores are framework-specific; the key point is that\n</span><span style=\"color:#75715e\"></span>      <span style=\"color:#75715e\">// both clients can share the same underlying session storage.\n</span><span style=\"color:#75715e\"></span>      <span style=\"color:#a6e22e\">transactionStore</span>: <span style=\"color:#66d9ef\">new</span> <span style=\"color:#a6e22e\">CookieTransactionStore</span>({ <span style=\"color:#a6e22e\">secret</span> }),\n      <span style=\"color:#a6e22e\">stateStore</span>: <span style=\"color:#66d9ef\">new</span> <span style=\"color:#a6e22e\">StatefulStateStore</span>({ <span style=\"color:#a6e22e\">secret</span>, <span style=\"color:#a6e22e\">store</span>: <span style=\"color:#66d9ef\">sessionStore</span> }),\n    });\n  }\n\n  <span style=\"color:#66d9ef\">return</span> <span style=\"color:#a6e22e\">auth0Clients</span>[<span style=\"color:#a6e22e\">mode</span>];\n}\n</code></pre></div><p>Because both clients use the same session cookie name, the latest login naturally overwrites the previous one. The lifecycle is always <strong>Onboarding → Main</strong>.</p>\n<p>Next, let’s set up our signup and login routes:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#75715e\">// src/routes/auth/login.tsx  (Normal login)\n</span><span style=\"color:#75715e\"></span><span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">startLogin</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">createServerFn</span>({ <span style=\"color:#a6e22e\">method</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;GET&#34;</span> }).<span style=\"color:#a6e22e\">handler</span>(<span style=\"color:#66d9ef\">async</span> () <span style=\"color:#f92672\">=&gt;</span> {\n  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">url</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#66d9ef\">await</span> <span style=\"color:#a6e22e\">getAuth0</span>(<span style=\"color:#e6db74\">&#34;main&#34;</span>).<span style=\"color:#a6e22e\">startInteractiveLogin</span>();\n  <span style=\"color:#66d9ef\">throw</span> <span style=\"color:#a6e22e\">redirect</span>({ <span style=\"color:#a6e22e\">href</span>: <span style=\"color:#66d9ef\">url.href</span> });\n});\n</code></pre></div><div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#75715e\">// src/routes/auth/signup.tsx\n</span><span style=\"color:#75715e\"></span><span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">startSignup</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">createServerFn</span>({ <span style=\"color:#a6e22e\">method</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;GET&#34;</span> }).<span style=\"color:#a6e22e\">handler</span>(<span style=\"color:#66d9ef\">async</span> () <span style=\"color:#f92672\">=&gt;</span> {\n  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">url</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#66d9ef\">await</span> <span style=\"color:#a6e22e\">getAuth0</span>(<span style=\"color:#e6db74\">&#34;onboarding&#34;</span>).<span style=\"color:#a6e22e\">startInteractiveLogin</span>({\n    <span style=\"color:#a6e22e\">authorizationParams</span><span style=\"color:#f92672\">:</span> { <span style=\"color:#a6e22e\">screen_hint</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;signup&#34;</span> },\n  });\n  <span style=\"color:#66d9ef\">throw</span> <span style=\"color:#a6e22e\">redirect</span>({ <span style=\"color:#a6e22e\">href</span>: <span style=\"color:#66d9ef\">url.href</span> });\n});\n\n<span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">startOnboardingLogin</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">createServerFn</span>({ <span style=\"color:#a6e22e\">method</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;GET&#34;</span> }).<span style=\"color:#a6e22e\">handler</span>(<span style=\"color:#66d9ef\">async</span> () <span style=\"color:#f92672\">=&gt;</span> {\n  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">url</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#66d9ef\">await</span> <span style=\"color:#a6e22e\">getAuth0</span>(<span style=\"color:#e6db74\">&#34;onboarding&#34;</span>).<span style=\"color:#a6e22e\">startInteractiveLogin</span>({\n    <span style=\"color:#a6e22e\">authorizationParams</span><span style=\"color:#f92672\">:</span> { <span style=\"color:#a6e22e\">prompt</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;login&#34;</span> },\n  });\n  <span style=\"color:#66d9ef\">throw</span> <span style=\"color:#a6e22e\">redirect</span>({ <span style=\"color:#a6e22e\">href</span>: <span style=\"color:#66d9ef\">url.href</span> });\n});\n\n<span style=\"color:#66d9ef\">export</span> <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">Route</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">createFileRoute</span>(<span style=\"color:#e6db74\">&#34;/auth/signup&#34;</span>)({\n  <span style=\"color:#a6e22e\">preload</span>: <span style=\"color:#66d9ef\">false</span>,\n  <span style=\"color:#a6e22e\">loader</span><span style=\"color:#f92672\">:</span> ({ <span style=\"color:#a6e22e\">location</span> }) <span style=\"color:#f92672\">=&gt;</span> {\n    <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">mode</span> <span style=\"color:#f92672\">=</span>\n      <span style=\"color:#66d9ef\">typeof</span> <span style=\"color:#a6e22e\">location</span>.<span style=\"color:#a6e22e\">search</span> <span style=\"color:#f92672\">===</span> <span style=\"color:#e6db74\">&#34;object&#34;</span> <span style=\"color:#f92672\">&amp;&amp;</span>\n      <span style=\"color:#a6e22e\">location</span>.<span style=\"color:#a6e22e\">search</span> <span style=\"color:#f92672\">!==</span> <span style=\"color:#66d9ef\">null</span> <span style=\"color:#f92672\">&amp;&amp;</span>\n      (<span style=\"color:#a6e22e\">location</span>.<span style=\"color:#a6e22e\">search</span> <span style=\"color:#66d9ef\">as</span> <span style=\"color:#a6e22e\">Record</span>&lt;<span style=\"color:#f92672\">string</span><span style=\"color:#960050;background-color:#1e0010\">,</span> <span style=\"color:#a6e22e\">unknown</span>&gt;)[<span style=\"color:#e6db74\">&#34;mode&#34;</span>] <span style=\"color:#f92672\">===</span> <span style=\"color:#e6db74\">&#34;login&#34;</span>\n        <span style=\"color:#f92672\">?</span> <span style=\"color:#e6db74\">&#34;login&#34;</span>\n        <span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;signup&#34;</span>;\n\n    <span style=\"color:#66d9ef\">return</span> <span style=\"color:#a6e22e\">mode</span> <span style=\"color:#f92672\">===</span> <span style=\"color:#e6db74\">&#34;login&#34;</span> <span style=\"color:#f92672\">?</span> <span style=\"color:#a6e22e\">startOnboardingLogin</span>() <span style=\"color:#f92672\">:</span> <span style=\"color:#a6e22e\">startSignup</span>();\n  },\n});\n</code></pre></div><p>Notice that each endpoint uses a different client: onboarding for signup, and main for actual login. Remember why we do this: the main client will prevent new signups from logging in because they aren’t part of an organisation yet (we’ll deal with aborted onboarding later). The onboarding client allows a new user to log in without being part of an organisation.</p>\n<p>Next, let’s add our callback routes so we can handle token requests.</p>\n<h4 id=\"main-app-callback\">Main app callback</h4>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#75715e\">// src/routes/auth/callback/main.tsx\n</span><span style=\"color:#75715e\"></span><span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">handleMainCallback</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">createServerFn</span>({ <span style=\"color:#a6e22e\">method</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;GET&#34;</span> }).<span style=\"color:#a6e22e\">handler</span>(<span style=\"color:#66d9ef\">async</span> () <span style=\"color:#f92672\">=&gt;</span> {\n  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">url</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">getRequestUrl</span>();\n\n  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">error</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">url</span>.<span style=\"color:#a6e22e\">searchParams</span>.<span style=\"color:#66d9ef\">get</span>(<span style=\"color:#e6db74\">&#34;error&#34;</span>);\n  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">description</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">url</span>.<span style=\"color:#a6e22e\">searchParams</span>.<span style=\"color:#66d9ef\">get</span>(<span style=\"color:#e6db74\">&#34;error_description&#34;</span>) <span style=\"color:#f92672\">??</span> <span style=\"color:#e6db74\">&#34;&#34;</span>;\n\n  <span style=\"color:#75715e\">// The user has an Auth0 account but isn&#39;t a member of any organisation yet.\n</span><span style=\"color:#75715e\"></span>  <span style=\"color:#66d9ef\">if</span> (\n    <span style=\"color:#a6e22e\">error</span> <span style=\"color:#f92672\">===</span> <span style=\"color:#e6db74\">&#34;invalid_request&#34;</span> <span style=\"color:#f92672\">&amp;&amp;</span>\n    <span style=\"color:#a6e22e\">description</span>.<span style=\"color:#a6e22e\">includes</span>(<span style=\"color:#e6db74\">&#34;client requires organization membership&#34;</span>)\n  ) {\n    <span style=\"color:#66d9ef\">throw</span> <span style=\"color:#a6e22e\">redirect</span>({ <span style=\"color:#a6e22e\">to</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;/auth/signup&#34;</span>, <span style=\"color:#a6e22e\">search</span><span style=\"color:#f92672\">:</span> { <span style=\"color:#a6e22e\">mode</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;login&#34;</span> } });\n  }\n\n  <span style=\"color:#66d9ef\">await</span> <span style=\"color:#a6e22e\">getAuth0</span>(<span style=\"color:#e6db74\">&#34;main&#34;</span>).<span style=\"color:#a6e22e\">completeInteractiveLogin</span>(<span style=\"color:#a6e22e\">url</span>);\n\n  <span style=\"color:#75715e\">// Redirect to the main application\n</span><span style=\"color:#75715e\"></span>  <span style=\"color:#66d9ef\">throw</span> <span style=\"color:#a6e22e\">redirect</span>({ <span style=\"color:#a6e22e\">to</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;/&#34;</span> });\n});\n</code></pre></div><p>In this callback we capture any errors from Auth0 post-login. If a user logs in and they don’t have an organisation, Auth0 will return an error containing the string <code>client requires organization membership</code>. When we see that, we redirect the user to log in through the onboarding client again, which forces them back down the onboarding flow.</p>\n<h4 id=\"onboarding-app-callback\">Onboarding app callback</h4>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#75715e\">// src/routes/auth/callback/onboarding.tsx\n</span><span style=\"color:#75715e\"></span><span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">handleOnboardingCallback</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">createServerFn</span>({ <span style=\"color:#a6e22e\">method</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;GET&#34;</span> }).<span style=\"color:#a6e22e\">handler</span>(\n  <span style=\"color:#66d9ef\">async</span> () <span style=\"color:#f92672\">=&gt;</span> {\n    <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">url</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">getRequestUrl</span>();\n    <span style=\"color:#66d9ef\">await</span> <span style=\"color:#a6e22e\">getAuth0</span>(<span style=\"color:#e6db74\">&#34;onboarding&#34;</span>).<span style=\"color:#a6e22e\">completeInteractiveLogin</span>(<span style=\"color:#a6e22e\">url</span>);\n    <span style=\"color:#66d9ef\">throw</span> <span style=\"color:#a6e22e\">redirect</span>({ <span style=\"color:#a6e22e\">to</span><span style=\"color:#f92672\">:</span> <span style=\"color:#e6db74\">&#34;/onboarding&#34;</span> });\n  },\n);\n</code></pre></div><p>We always redirect to <code>/onboarding</code> if a user logs in using the onboarding client. <code>/onboarding</code> will eventually redirect the user back to the actual app once they’ve set up their user and organisation, so even if an already-onboarded user goes down this flow they will naturally end up in the right place.</p>\n<p>Now when a new user signs up, they will log in and go through the onboarding process, which is protected from malicious spamming through authentication! You are free to set up these screens however you like; the only thing you need to follow is the order, which should be <strong>set up user → set up organisation</strong>.</p>\n<p><strong>&ldquo;Why do I need to do it in this order?&quot;</strong> Remember the API endpoints: the <code>POST /organisations</code> endpoint returns an object including a redirect URI that will direct the user to log in to the newly created organisation. You could swap the flow and return the redirect from the user endpoint, but it doesn’t make sense to a consumer, and you would have extra work to fetch the organisation from the DB to retrieve the Auth0 ID to then pass down to the redirect.</p>\n<p>So now we’ve gone over that, once the user has finished onboarding we will use the redirect. Simply take the redirect URL from the <code>POST /organisations</code> response and do:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">result</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#66d9ef\">await</span> <span style=\"color:#a6e22e\">response</span>.<span style=\"color:#a6e22e\">json</span>();\nwindow.<span style=\"color:#a6e22e\">location</span>.<span style=\"color:#a6e22e\">href</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">result</span>.<span style=\"color:#a6e22e\">redirect</span>;\n</code></pre></div><p>This will redirect the user back to Auth0, which will then log them into the organisation and send them back to the main app callback!</p>\n<p>That is the whole setup finished! A new user can now sign up, log in, onboard, and get into their organisation, being directed back to onboarding on the next login if they abandon it mid-journey.</p>\n",
        "date_published": "2026-06-19T22:39:52+01:00",
        "url": "https://benrants.com/2026/06/19/auth-organisations-self-service-signup.html",
        "tags": ["auth0"]
      }
  ]
}
