Quickstart: Login with Flowsta
This guide will walk you through integrating "Login with Flowsta" into your application in under 15 minutes.
What You'll Build
A complete OAuth integration with:
- "Login with Flowsta" button in your frontend
- OAuth callback handler
- Token exchange in your backend
- User profile retrieval
Prerequisites
- A Flowsta developer account (sign up here)
- A web application (static HTML or modern framework)
Choose Your Integration Method
Flowsta offers multiple ways to integrate "Sign in with Flowsta" depending on your tech stack:
🚀 NPM Package (Recommended for Modern Apps)
Best for React, Vue, Qwik, or any app with a build system.
- ✅ Pre-built components
- ✅ TypeScript support
- ✅ Automatic PKCE generation
- ✅ Framework-specific optimizations
Continue with this guide below →
🌐 Vanilla JavaScript (No Build Tools)
Best for static HTML sites or simple JavaScript projects.
- ✅ No npm required
- ✅ Works with any (or no) framework
- ✅ Copy-paste ready examples
- ✅ CDN or self-hosted buttons
🎨 Direct Button Download
Best for quick prototyping or custom implementations.
- ✅ Download button images (SVG/PNG)
- ✅ Hotlink from CDN
- ✅ Manual OAuth URL construction
- ✅ Maximum flexibility
Step 1: Create Your App
1.1 Register Your Application
Click "Create New App"
Enter your app details:
- Name: My Awesome App
- Description: A cool app using Flowsta Auth
Copy your Client ID:
Client ID: flowsta_app_abc123...
No Client Secret Needed
Flowsta uses OAuth 2.0 with PKCE, which means you don't need a client secret for browser or mobile apps. Your Client ID is all you need!
1.2 Configure OAuth Settings
In your app's details page, scroll to "OAuth Settings" and click "Edit Settings":
Add Redirect URIs
Add the URLs where users will be redirected after login:
http://localhost:3000/auth/callback (for development)
https://yourapp.com/auth/callback (for production)Multiple Redirect URIs
You can add up to 10 redirect URIs per app. This allows you to use different URLs for development, staging, and production.
Select Scopes
Choose which data your app needs:
- ✅ profile - User ID, display name, username, DID, agent public key, profile picture (always available)
- ☐ email - Email address and verification status (requires user permission)
Email Scope
The email scope requires explicit user permission. Users will see a consent screen and must approve email access through their Flowsta account settings.
Add Branding (Optional)
Make your consent screen look professional:
- Logo URL:
https://yourapp.com/logo.png(recommended: 200x200px) - Privacy Policy:
https://yourapp.com/privacy - Terms of Service:
https://yourapp.com/terms
Click "Save OAuth Settings".
Step 2: Install the Button Widget
The @flowsta/login-button package provides pre-built login buttons for React, Vue, Qwik, and Vanilla JS.
npm install @flowsta/login-buttonFramework Support
- ✅ React 16.8+ (hooks)
- ✅ Vue 3 (composition API)
- ✅ Qwik (resumable)
- ✅ Vanilla JavaScript (any framework or no framework)
Step 3: Add the Login Button
Choose your framework:
// src/components/LoginButton.tsx
import { FlowstaLoginButton } from '@flowsta/login-button/react';
export function LoginButton() {
const handleSuccess = (data) => {
// data.code = authorization code
// data.state = your state parameter (for CSRF protection)
console.log('Authorization code:', data.code);
// Send to your backend for token exchange
window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
};
const handleError = (error) => {
console.error('Login error:', error);
alert(`Login failed: ${error.message}`);
};
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="http://localhost:3000/auth/callback"
scope="profile email"
variant="dark_pill"
onSuccess={handleSuccess}
onError={handleError}
/>
);
}<!-- src/components/LoginButton.vue -->
<script setup lang="ts">
import { FlowstaLoginButtonVue } from '@flowsta/login-button/vue';
const handleSuccess = (data: { code: string; state: string }) => {
console.log('Authorization code:', data.code);
// Send to your backend for token exchange
window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
};
const handleError = (error: Error) => {
console.error('Login error:', error);
alert(`Login failed: ${error.message}`);
};
</script>
<template>
<FlowstaLoginButtonVue
client-id="your_client_id"
redirect-uri="http://localhost:3000/auth/callback"
scope="profile email"
variant="dark_pill"
@success="handleSuccess"
@error="handleError"
/>
</template>// src/components/login-button.tsx
import { component$, $ } from '@builder.io/qwik';
import { FlowstaLoginButtonQwik } from '@flowsta/login-button/qwik';
export default component$(() => {
const handleSuccess = $((data: { code: string; state: string }) => {
console.log('Authorization code:', data.code);
// Send to your backend for token exchange
window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
});
const handleError = $((error: Error) => {
console.error('Login error:', error);
alert(`Login failed: ${error.message}`);
});
return (
<FlowstaLoginButtonQwik
clientId="your_client_id"
redirectUri="http://localhost:3000/auth/callback"
scope="profile email"
variant="dark_pill"
onSuccess$={handleSuccess}
onError$={handleError}
/>
);
});<!-- index.html -->
<div id="login-container"></div>
<script type="module">
import { FlowstaLoginButton } from '@flowsta/login-button/vanilla';
const button = new FlowstaLoginButton({
clientId: 'your_client_id',
redirectUri: 'http://localhost:3000/auth/callback',
scope: 'profile email',
variant: 'dark_pill',
onSuccess: (data) => {
console.log('Authorization code:', data.code);
window.location.href = `/auth/callback?code=${data.code}&state=${data.state}`;
},
onError: (error) => {
console.error('Login error:', error);
alert(`Login failed: ${error.message}`);
},
});
button.mount('#login-container');
</script>Step 4: Create Backend Callback Handler
When Flowsta redirects back to your app, your backend needs to exchange the authorization code for an access token.
// routes/auth.js
import express from 'express';
const router = express.Router();
router.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state for CSRF protection
const storedState = req.session.oauthState;
if (!state || state !== storedState) {
return res.status(400).send('Invalid state parameter');
}
// Retrieve stored code_verifier (from PKCE flow)
const codeVerifier = req.session.codeVerifier;
try {
// Exchange authorization code for tokens
const tokenResponse = await fetch('https://auth-api.flowsta.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: 'http://localhost:3000/auth/callback',
client_id: process.env.FLOWSTA_CLIENT_ID,
code_verifier: codeVerifier, // PKCE - no client_secret needed!
}),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.json();
throw new Error(error.error || 'Token exchange failed');
}
const { access_token, refresh_token, expires_in } = await tokenResponse.json();
// Fetch user profile
const userResponse = await fetch('https://auth-api.flowsta.com/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${access_token}`,
},
});
const user = await userResponse.json();
// Store tokens in session
req.session.accessToken = access_token;
req.session.refreshToken = refresh_token;
req.session.user = user;
// Redirect to app homepage
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth callback error:', error);
res.status(500).send('Authentication failed');
}
});
export default router;// app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get('code');
const state = searchParams.get('state');
// Verify state
const cookieStore = cookies();
const storedState = cookieStore.get('oauth_state')?.value;
const codeVerifier = cookieStore.get('code_verifier')?.value;
if (!state || state !== storedState) {
return NextResponse.json({ error: 'Invalid state' }, { status: 400 });
}
try {
// Exchange code for tokens
const tokenResponse = await fetch('https://auth-api.flowsta.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`,
client_id: process.env.FLOWSTA_CLIENT_ID!,
code_verifier: codeVerifier, // PKCE - no client_secret needed!
}),
});
const { access_token, refresh_token } = await tokenResponse.json();
// Fetch user info
const userResponse = await fetch('https://auth-api.flowsta.com/oauth/userinfo', {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const user = await userResponse.json();
// Set secure cookies
const response = NextResponse.redirect(new URL('/dashboard', request.url));
response.cookies.set('access_token', access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
return response;
} catch (error) {
console.error('OAuth error:', error);
return NextResponse.redirect(new URL('/login?error=auth_failed', request.url));
}
}Step 5: Store Credentials Securely
Create a .env file in your project root:
# .env
FLOWSTA_CLIENT_ID=your_client_id_hereLoad environment variables in your app:
// Node.js
import dotenv from 'dotenv';
dotenv.config();
const clientId = process.env.FLOWSTA_CLIENT_ID;PKCE Security
With PKCE, your Client ID can safely be in frontend code - there's no secret to protect. The code_verifier generated per-session provides security.
Step 6: Test Your Integration
6.1 Start Your App
npm run devVisit http://localhost:3000
6.2 Click "Login with Flowsta"
You'll be redirected to login.flowsta.com
6.3 Login or Sign Up
- Existing users: Enter email + password
- New users: Create a Flowsta account
6.4 Review Consent Screen
See which data your app is requesting:
- ✅ Profile information
- ✅ Email address (if requested)
6.5 Approve Access
Click "Authorize" to grant access
6.6 Return to Your App
You'll be redirected back with an authorization code, which your backend exchanges for an access token.
6.7 You're Logged In! 🎉
Check your console logs to see the user data:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"preferred_username": "johndoe",
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"agent_pub_key": "uhCAk...",
"profile_picture": "https://...",
"email": "john@example.com",
"email_verified": true
}Step 7: Handle User Sessions
Store User in Session
// After successful token exchange
req.session.user = {
id: user.userId,
name: user.displayName,
email: user.email,
did: user.did,
};Protect Routes
// Middleware
function requireAuth(req, res, next) {
if (!req.session.user) {
return res.redirect('/login');
}
next();
}
// Protected route
app.get('/dashboard', requireAuth, (req, res) => {
res.render('dashboard', { user: req.session.user });
});Logout
app.post('/logout', async (req, res) => {
// Revoke refresh token
if (req.session.refreshToken) {
await fetch('https://auth-api.flowsta.com/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': process.env.FLOWSTA_CLIENT_ID,
},
body: JSON.stringify({
token: req.session.refreshToken,
token_type_hint: 'refresh_token',
}),
});
}
// Clear session
req.session.destroy();
res.redirect('/');
});Next Steps
📚 Deep Dive
- Button Widget - Customize appearance and behavior
- API Reference - Complete endpoint documentation
- Security Guide - Best practices and security patterns
🎨 Customization
- Try different button variants:
light_pill,neutral_rectangle, etc. - Add your app's logo to the consent screen
- Customize redirect behavior
🔐 Production Checklist
- [ ] Use HTTPS for all redirect URIs
- [ ] Implement CSRF protection with
stateparameter (SDK handles this automatically) - [ ] Use secure session storage (httpOnly cookies recommended)
- [ ] Implement token refresh logic
- [ ] Add error handling and user feedback
- [ ] Test with multiple user accounts
- [ ] Monitor OAuth audit logs in developer dashboard
No Client Secrets
With PKCE, you don't need to manage client secrets! Your Client ID can safely be in frontend code. The code_verifier provides security without secrets.
Common Issues
"Invalid redirect_uri" Error
Cause: The redirect URI doesn't match any URI configured in your app settings.
Solution: Go to your app settings and add the exact redirect URI (including protocol and port).
"Invalid client" Error
Cause: Client ID is incorrect.
Solution: Double-check your Client ID in the .env file or Developer Dashboard.
"Code has expired" Error
Cause: Authorization codes expire after 10 minutes.
Solution: Ensure your backend exchanges the code immediately after receiving it.
Email Not Returned
Cause: User denied the email scope or your app doesn't have email scope configured.
Solution:
- Ensure your app has the
emailscope enabled in OAuth settings - User must click "Authorize" on the consent screen to grant email access
- If user previously denied access, they'll need to re-authorize your app
Need Help?
- 💬 Discord: Join our community
- 🆘 Support: Find out about Flowsta support options
- 🐙 GitHub: github.com/WeAreFlowsta