Quickstart: Sign in with Flowsta
This guide will walk you through integrating "Sign in with Flowsta" into your application in under 15 minutes.
What You'll Build
A complete OAuth integration with:
- "Sign in with Flowsta" button in your frontend
- OAuth callback page at your redirect URI
- Token exchange in your backend
- User profile retrieval
Prerequisites
- A Flowsta developer account (sign up here)
- A web application (static HTML or modern framework)
How Users Sign In
Flowsta accounts are device-hosted: the user's identity is a keypair on their own device, backed by a 24-word recovery phrase and the Flowsta Vault app. There are no passwords. When your button redirects a user to login.flowsta.com, their Vault signs a one-time challenge to approve the sign-in - keys only they hold, no one in between. Your app just sees a standard OAuth flow.
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 and production.
Select Scopes
Choose which data your app needs:
- ✅ openid - User ID (always included)
- ☐ display_name - User's display name
- ☐ username - User's @username
- ☐ did - W3C Decentralized Identifier
- ☐ public_key - Holochain agent public key
- ☐ profile_picture - Profile picture
- ☐ email - Asks the user to share their address at consent (verified only; revocable)
- ☐ sign / verify - Access to Sign It endpoints
Email Scope
Flowsta stores no email for the account - the email scope asks the user to share their address at the consent screen. If they do, /oauth/userinfo returns email and email_verified for your app (verified addresses only); if they decline or later revoke your app, there is no email. Design your app to work without one.
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
The button generates PKCE parameters, stores them in sessionStorage, and performs a full-page redirect to login.flowsta.com. Nothing else happens in this page's JavaScript - the authorization code arrives at your redirect URI on a fresh page load (Step 4). The only callbacks that fire are onClick (just before the redirect) and onError (if the authorization URL could not be built).
Choose your framework:
// src/components/LoginButton.tsx
import { FlowstaLoginButton } from '@flowsta/login-button/react';
export function LoginButton() {
const handleError = (error) => {
console.error('Could not start login:', error);
alert(`Login failed: ${error.errorDescription || error.error}`);
};
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="http://localhost:3000/auth/callback"
scopes={['openid', 'display_name']}
variant="dark-pill"
onError={handleError}
/>
);
}<!-- src/components/LoginButton.vue -->
<script setup lang="ts">
import { FlowstaLoginButton } from '@flowsta/login-button/vue';
const handleError = (error: { error: string; errorDescription?: string }) => {
console.error('Could not start login:', error);
alert(`Login failed: ${error.errorDescription || error.error}`);
};
</script>
<template>
<FlowstaLoginButton
client-id="your_client_id"
redirect-uri="http://localhost:3000/auth/callback"
:scopes="['openid', 'display_name']"
variant="dark-pill"
@error="handleError"
/>
</template>// src/components/login-button.tsx
import { component$, $ } from '@builder.io/qwik';
import { FlowstaLoginButton } from '@flowsta/login-button/qwik';
export default component$(() => {
const handleError = $((error: { error: string; errorDescription?: string }) => {
console.error('Could not start login:', error);
});
return (
<FlowstaLoginButton
clientId="your_client_id"
redirectUri="http://localhost:3000/auth/callback"
scopes={['openid', 'display_name']}
variant="dark-pill"
onError$={handleError}
/>
);
});<!-- index.html -->
<div id="login-container"></div>
<script type="module">
import { initFlowstaLoginButton } from '@flowsta/login-button/vanilla';
initFlowstaLoginButton('#login-container', {
clientId: 'your_client_id',
redirectUri: 'http://localhost:3000/auth/callback',
scopes: ['openid', 'display_name'],
variant: 'dark-pill',
onError: (error) => {
console.error('Could not start login:', error);
alert(`Login failed: ${error.errorDescription}`);
},
});
</script>Step 4: Create the Callback Page
When the user approves the sign-in, Flowsta redirects the browser to your redirect URI with ?code=...&state=.... This is a fresh page load - create a page at that route which:
- Reads the
codeandstatefrom the URL - Validates the
state(CSRF protection) - Retrieves the PKCE
code_verifierthe button stored insessionStorage - Sends
code+code_verifierto your backend for the token exchange
// Runs on your /auth/callback page
import {
handleCallback,
validateState,
retrieveCodeVerifier,
} from '@flowsta/login-button';
async function completeSignIn() {
const { code, state, error, errorDescription } = handleCallback();
if (error) {
throw new Error(errorDescription || error);
}
if (!code) {
throw new Error('No authorization code received');
}
if (!state || !validateState(state)) {
throw new Error('State mismatch - possible CSRF attack');
}
// The button stored the verifier keyed by state; this retrieves and clears it
const codeVerifier = retrieveCodeVerifier(state);
if (!codeVerifier) {
throw new Error('Missing PKCE code verifier');
}
// Hand off to your backend for the token exchange
const response = await fetch('/api/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, code_verifier: codeVerifier }),
});
if (!response.ok) throw new Error('Authentication failed');
window.location.href = '/dashboard';
}
completeSignIn().catch((err) => {
console.error('OAuth callback error:', err);
});Browser-Only Apps
If you have no backend, you can do the token exchange directly from the callback page - PKCE makes this safe for public clients. See the Vanilla JS guide for a complete browser-only example.
Step 5: Exchange the Code in Your Backend
Your backend receives the code and code_verifier from the callback page, exchanges them for tokens, and creates a session.
// routes/auth.js
import express from 'express';
const router = express.Router();
router.post('/exchange', async (req, res) => {
const { code, code_verifier } = req.body;
if (!code || !code_verifier) {
return res.status(400).json({ error: 'Missing code or code_verifier' });
}
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, // 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;
res.json({ success: true });
} catch (error) {
console.error('OAuth exchange error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
export default router;// app/api/auth/exchange/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { code, code_verifier } = await request.json();
if (!code || !code_verifier) {
return NextResponse.json({ error: 'Missing code or code_verifier' }, { 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, // 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.json({ success: true, user });
response.cookies.set('access_token', access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24, // 24 hours (access token lifetime)
});
return response;
} catch (error) {
console.error('OAuth error:', error);
return NextResponse.json({ error: 'auth_failed' }, { status: 500 });
}
}Step 6: 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 7: Test Your Integration
7.1 Start Your App
npm run devVisit http://localhost:3000
7.2 Click "Sign in with Flowsta"
You'll be redirected to login.flowsta.com
7.3 Sign In or Sign Up
- Existing users: Approve the sign-in with the Flowsta Vault - the Vault signs a challenge with keys only the user holds. No password exists to enter.
- New users: Create a Flowsta account (they'll set up the Vault and a 24-word recovery phrase)
7.4 Review Consent Screen
The user sees which data your app is requesting - for example, profile information.
7.5 Approve Access
Click "Authorize" to grant access
7.6 Return to Your App
The browser lands on your callback page with an authorization code, which your backend exchanges for an access token.
7.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:flowsta:uhCAk7JpEWfkiV_RdAFfCnRZcJ9PwJR4yTLN-E3EcVU7KYCnRRZc",
"agent_pub_key": "uhCAk...",
"profile_picture": "https://...",
"has_custom_picture": true
}No Email in the Response?
Not a bug - email appears only when the user explicitly shared their address at the consent screen (and the email scope was requested). No sharing, no email.
Step 8: Handle User Sessions
Store User in Session
// After successful token exchange
req.session.user = {
id: user.sub,
name: user.name,
username: user.preferred_username,
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',
},
body: JSON.stringify({
token: req.session.refreshToken,
token_type_hint: 'refresh_token',
client_id: process.env.FLOWSTA_CLIENT_ID,
}),
});
}
// 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: The user didn't share their address at the consent screen (or revoked your app, which deletes the shared copy). Flowsta's server stores only a hash - an email exists in /oauth/userinfo only after the user explicitly shares it.
Solution: Don't require the email field. If the user didn't share one and your app needs it, ask them directly after sign-in.
Need Help?
- 💬 Discord: Join our community
- 🆘 Support: Find out about Flowsta support options
- 🐙 GitHub: github.com/WeAreFlowsta