Skip to content

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-web

Without 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.

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)

cd LBS.Foundry
pnpm install  # Sets up workspace linking automatically

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)

pnpm web:dev

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:

{
  "dependencies": {
    "@luckboxstudios/foundry-sdk": "workspace:*"
  }
}

This means foundry-web uses the local SDK source during development, not the published npm package.

Python SDK Local Development

Python:

pip install -e sdk/python  # Editable install from local source

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>

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

# From repo root
pip install -e sdk/python

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:

python test_auth.py

Testing Authentication End-to-End

1. Start the Backend

# Run Aspire host (includes API and database)
cd src/Aspire/LBS.AspireHost
dotnet run

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):

cd src/Apps/foundry-web
pnpm dev

Navigate to http://localhost:5173/login and test the login form.

Python:

python test_auth.py

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:

VITE_API_BASE_URL=http://localhost:5000

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)

# Re-run pnpm install from root to re-establish workspace links
cd LBS.Foundry
pnpm install
# 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

# Regenerate from C# assemblies
pnpm sdk:refresh

Python SDK Changes Not Reflected

# Reinstall in editable mode
pip uninstall foundry-sdk
pip install -e sdk/python

Login Returns 401

  • Check backend is running (dotnet run in 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 autoRefresh is true (default)
  • Verify refresh token is stored correctly
  • Check backend /auth/refresh endpoint is working
  • Look for 401 errors in network tab

Best Practices

  1. Use the Auth Store Pattern - Centralize authentication logic in a Svelte store
  2. Store Tokens Securely - Use localStorage for web, secure storage for mobile
  3. Handle Token Expiration - Let SDK auto-refresh handle it, but have fallback to login
  4. Clear Tokens on Logout - Always clear local storage when logging out
  5. Test Error Cases - Invalid credentials, expired tokens, network failures
  6. Use Environment Variables - Never hardcode API URLs or credentials
  7. Enable Debug Logging - During development, log authentication events

Next Steps

  • Protect Routes: Add authentication guards to protected routes
  • Role-Based Access: Check user.roles for 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

Additional Resources