SDK Local Development Guide¶
This guide explains how to use the Foundry SDK locally during development, including linking the SDK to your applications and testing authentication.
First Time Setup Required
If you've just cloned the repository, you must build the SDK before running foundry-web:
cd LBS.Foundry pnpm install # Install dependencies and establish workspace links pnpm sdk:refresh # Build .NET + generate SDK + compile TypeScript pnpm web:dev # Now you can run foundry-webWithout this step, you'll get:
Failed to resolve entry for package "@luckboxstudios/foundry-sdk"
Overview¶
The Foundry SDK comes in two flavors:
- Python SDK (sdk/python/) - For backend services, scripts, and Python applications
- TypeScript SDK (sdk/typescript/) - For frontend applications (foundry-web) and TypeScript services
Both SDKs now support JWT authentication with automatic token refresh.
Quick Start - pnpm Workspace (Recommended)¶
The SDK uses a pnpm workspace for seamless local development. Changes to C# types are instantly available in foundry-web without publishing.
Initial Setup (Once)¶
After Making C# Contract Changes¶
# One command to rebuild everything:
pnpm sdk:refresh
# This runs:
# 1. dotnet build LBS.slnx
# 2. SDK generator (scans assemblies, generates TypeScript)
# 3. TypeScript compilation
Run foundry-web (Uses Local SDK)¶
Available Commands¶
From repo root:
| Command | Description |
|---------|-------------|
| pnpm sdk:refresh | Build .NET + regenerate SDK + compile TypeScript |
| pnpm sdk:generate | Regenerate TypeScript from Debug build |
| pnpm sdk:generate:release | Regenerate from Release build |
| pnpm sdk:build | Compile TypeScript only |
| pnpm web:dev | Run foundry-web with local SDK |
| pnpm web:build | Build foundry-web for production |
From sdk/typescript:
| Command | Description |
|---------|-------------|
| pnpm generate | Regenerate TypeScript types |
| pnpm generate:release | Regenerate from Release build |
| pnpm build | Compile TypeScript |
How the Workspace Works¶
The pnpm-workspace.yaml at repo root links:
- sdk/typescript - The SDK source
- src/Apps/foundry-web - The frontend app
In foundry-web/package.json:
This means foundry-web uses the local SDK source during development, not the published npm package.
Python SDK Local Development¶
Python:
TypeScript SDK Local Development (Legacy - Use Workspace Instead)¶
4. Integrate Authentication in foundry-web¶
Update your login page (src/Apps/foundry-web/src/routes/login/+page.svelte):
<script lang="ts">
import { FoundryClient, FoundryError } from '@luckboxstudios/foundry-sdk';
import { goto } from '$app/navigation';
let email = $state('');
let password = $state('');
let error = $state('');
let loading = $state(false);
// Initialize Foundry client
const client = new FoundryClient({
baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:5000',
autoRefresh: true
});
async function handleLogin() {
loading = true;
error = '';
try {
const result = await client.login(email, password);
console.log('Login successful:', result.user.email);
console.log('Roles:', result.user.roles);
// Store tokens in localStorage or session
localStorage.setItem('foundry_access_token', result.token);
localStorage.setItem('foundry_refresh_token', result.refreshToken);
// Redirect to dashboard
goto('/dashboard');
} catch (err) {
if (err instanceof FoundryError) {
if (err.statusCode === 401) {
error = 'Invalid email or password';
} else {
error = `Login failed: ${err.message}`;
}
} else {
error = 'An unexpected error occurred';
}
console.error('Login error:', err);
} finally {
loading = false;
}
}
</script>
<form on:submit|preventDefault={handleLogin}>
<div>
<label for="email">Email</label>
<input
id="email"
type="email"
bind:value={email}
required
disabled={loading}
/>
</div>
<div>
<label for="password">Password</label>
<input
id="password"
type="password"
bind:value={password}
required
disabled={loading}
/>
</div>
{#if error}
<div class="error">{error}</div>
{/if}
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Log in'}
</button>
</form>
5. Create an Auth Store (Recommended)¶
Create src/Apps/foundry-web/src/lib/stores/auth.svelte.ts:
import { FoundryClient, type LoginResponse, type UserInfo } from '@luckboxstudios/foundry-sdk';
import { browser } from '$app/environment';
class AuthStore {
client: FoundryClient;
user = $state<UserInfo | null>(null);
isAuthenticated = $state(false);
isLoading = $state(true);
constructor() {
this.client = new FoundryClient({
baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:5000',
autoRefresh: true
});
// Initialize from localStorage on client side
if (browser) {
this.initializeFromStorage();
}
}
private async initializeFromStorage() {
const token = localStorage.getItem('foundry_access_token');
const refreshToken = localStorage.getItem('foundry_refresh_token');
if (token && refreshToken) {
// Set tokens in client
this.client = new FoundryClient({
baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:5000',
authToken: token,
refreshToken: refreshToken,
autoRefresh: true
});
try {
// Verify token is still valid by getting current user
this.user = await this.client.getCurrentUser();
this.isAuthenticated = true;
} catch (error) {
// Token invalid, clear storage
this.logout();
}
}
this.isLoading = false;
}
async login(email: string, password: string): Promise<void> {
const result = await this.client.login(email, password);
// Store tokens
if (browser) {
localStorage.setItem('foundry_access_token', result.token);
localStorage.setItem('foundry_refresh_token', result.refreshToken);
}
this.user = result.user;
this.isAuthenticated = true;
}
async logout(): Promise<void> {
try {
await this.client.logout();
} catch (error) {
console.error('Logout error:', error);
}
// Clear local state
this.user = null;
this.isAuthenticated = false;
if (browser) {
localStorage.removeItem('foundry_access_token');
localStorage.removeItem('foundry_refresh_token');
}
}
async refreshToken(): Promise<void> {
const result = await this.client.refreshToken();
if (browser) {
localStorage.setItem('foundry_access_token', result.token);
localStorage.setItem('foundry_refresh_token', result.refreshToken);
}
}
}
export const auth = new AuthStore();
Then use it in your login page:
<script lang="ts">
import { auth } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { FoundryError } from '@luckboxstudios/foundry-sdk';
let email = $state('');
let password = $state('');
let error = $state('');
let loading = $state(false);
async function handleLogin() {
loading = true;
error = '';
try {
await auth.login(email, password);
goto('/dashboard');
} catch (err) {
if (err instanceof FoundryError && err.statusCode === 401) {
error = 'Invalid email or password';
} else {
error = 'An unexpected error occurred';
}
} finally {
loading = false;
}
}
</script>
Python SDK Local Development¶
1. Install SDK in Editable Mode¶
This allows you to make changes to the SDK and see them immediately without reinstalling.
2. Using the Python SDK¶
Create a test script test_auth.py:
import asyncio
from foundry_sdk import FoundryClient, FoundryClientConfig, FoundryError
async def main():
# Create client
client = FoundryClient(FoundryClientConfig(
base_url="http://localhost:5000",
auto_refresh=True
))
try:
# Login
result = await client.login("user@example.com", "password")
print(f"Login successful")
print(f" User: {result.user.email}")
print(f" Roles: {result.user.roles}")
print(f" Token expires in: {result.expires_in} seconds")
# Test authenticated query
from foundry_sdk.modules.core.queries import current_user
user_data = await client.query(current_user())
print(f"Query successful: {user_data.data}")
# Logout
await client.logout()
print(f"Logged out successfully")
except FoundryError as e:
print(f"Error: {e}")
if e.status_code:
print(f" Status code: {e.status_code}")
if __name__ == "__main__":
asyncio.run(main())
Run it:
Testing Authentication End-to-End¶
1. Start the Backend¶
The API will be available at http://localhost:5000.
2. Create a Test User¶
Use the User Seeding import or create via SQL:
# Using Python SDK
python -c "
import asyncio
from foundry_sdk import FoundryClient, FoundryClientConfig
from foundry_sdk.modules.core.imports import user_seeding
async def seed():
client = FoundryClient(FoundryClientConfig(
base_url='http://localhost:5000',
auth_token='your-admin-token' # Or use service account
))
result = await client.import_data('UserSeedingImport', user_seeding(
enabled=True,
skip_if_users_exist=False,
users=[{
'email': 'test@example.com',
'display_name': 'Test User',
'roles': ['Member']
}]
))
print(result)
asyncio.run(seed())
"
3. Test Login Flow¶
TypeScript (foundry-web):
Navigate to http://localhost:5173/login and test the login form.
Python:
4. Verify Token Refresh¶
Modify the test to wait for token expiration:
// In browser console after login
console.log('Waiting for token to expire...');
await new Promise(resolve => setTimeout(resolve, 65000)); // Wait 65 seconds (token expires in 60)
// Try a query - should auto-refresh
const result = await client.query(myQuery);
console.log('Token auto-refreshed:', result);
Environment Variables¶
foundry-web (.env)¶
Create src/Apps/foundry-web/.env:
Python¶
Set environment variable or pass in config:
import os
client = FoundryClient(FoundryClientConfig(
base_url=os.getenv('FOUNDRY_API_URL', 'http://localhost:5000')
))
Troubleshooting¶
TypeScript SDK Not Found (Workspace)¶
TypeScript SDK Not Found (Legacy Link Method)¶
# Unlink and relink
cd sdk/typescript
pnpm unlink --global
pnpm link --global
cd src/Apps/foundry-web
pnpm unlink --global @luckboxstudios/foundry-sdk
pnpm link --global @luckboxstudios/foundry-sdk
SDK Types Out of Date¶
Python SDK Changes Not Reflected¶
Login Returns 401¶
- Check backend is running (
dotnet runin AspireHost) - Verify user exists in database
- Check password is correct
- Look at backend logs for authentication errors
CORS Errors¶
Update backend CORS configuration in src/Aspire/LBS.AspireHost/Program.cs:
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:5173") // Vite dev server
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
Token Refresh Not Working¶
- Check
autoRefreshistrue(default) - Verify refresh token is stored correctly
- Check backend
/auth/refreshendpoint is working - Look for 401 errors in network tab
Best Practices¶
- Use the Auth Store Pattern - Centralize authentication logic in a Svelte store
- Store Tokens Securely - Use
localStoragefor web, secure storage for mobile - Handle Token Expiration - Let SDK auto-refresh handle it, but have fallback to login
- Clear Tokens on Logout - Always clear local storage when logging out
- Test Error Cases - Invalid credentials, expired tokens, network failures
- Use Environment Variables - Never hardcode API URLs or credentials
- Enable Debug Logging - During development, log authentication events
Next Steps¶
- Protect Routes: Add authentication guards to protected routes
- Role-Based Access: Check
user.rolesfor authorization - Persistent Sessions: Consider refresh token rotation and remember-me functionality
- Error Boundaries: Add global error handling for authentication failures
- Loading States: Show spinners during login/logout operations