foundation-sdk
v0.3.6
Published
TypeScript SDK for Foundation
Downloads
330
Maintainers
Readme
foundation-sdk
TypeScript SDK for the Foundation platform. Handles authentication, data, files, integrations, and account management with one client.
Installation
npm install foundation-sdkIf you're using Cognito or Auth0 (most apps), install the corresponding peer dependency:
npm install aws-amplify # for Cognito
npm install @auth0/auth0-spa-js # for Auth0Quick Start
import { createFoundation } from 'foundation-sdk'
import { cognitoAuth } from 'foundation-sdk/cognito' // or 'foundation-sdk/auth0'
const foundation = await createFoundation({
configUrl: 'https://backend.example.com/api/v1/public/init',
tenantId: 'your-tenant-id',
appId: 'your-app-id',
auth: cognitoAuth
})
// Now use it
await foundation.auth.signIn('[email protected]', 'password')
const { items: projects } = await foundation.db.list('projects')In production deployments, the SDK reads /foundation-env.json to discover the config URL and identifiers, so the call is just:
const foundation = await createFoundation({ auth: cognitoAuth })Configuration
interface FoundationConfig {
/** Public bootstrap endpoint URL. If omitted, reads from /foundation-env.json */
configUrl?: string
/** Tenant identifier */
tenantId?: string
/** Application identifier */
appId?: string
/** Override API base URL — useful with a dev proxy ('/api') */
baseUrl?: string
/** Auth provider factory or instance */
auth?: AuthClient | AuthProvider
}When /foundation-env.json is present, its values override configUrl, tenantId, appId, and clear baseUrl (the proxy is dev-only).
configUrl is fetched exactly as provided. It should already be the public bootstrap URL, usually https://.../api/v1/public/init. After auth, the SDK separately fetches authenticated backend config from the API base URL.
createFoundation(...) resolves with a fully initialized instance. You do not need to await foundation.ready separately.
Authentication
The SDK supports Cognito, Auth0, or no auth. Pick a provider entry point based on what your Foundation app uses:
// Cognito
import { cognitoAuth } from 'foundation-sdk/cognito'
const f = await createFoundation({ auth: cognitoAuth })
// Auth0
import { auth0Auth } from 'foundation-sdk/auth0'
const f = await createFoundation({ auth: auth0Auth })Or auto-register via side-effect import:
import 'foundation-sdk/cognito' // registers cognito as a known provider
const f = await createFoundation({}) // SDK reads provider from config and uses itSign in / sign up
signIn and signUp return result objects so you can branch on the outcome:
const result = await foundation.auth.signIn(email, password)
if (result.isSignedIn) {
router.push('/dashboard')
} else if (result.nextStep?.signInStep === 'CONFIRM_SIGN_UP') {
router.push({ path: '/confirm', query: { email } })
} else {
console.error(`Sign-in needs additional step: ${result.nextStep?.signInStep}`)
}
const signUpResult = await foundation.auth.signUp(email, password, { name })
if (signUpResult.isSignUpComplete) {
// verification email sent — user can sign in
} else if (signUpResult.nextStep?.signUpStep === 'CONFIRM_SIGN_UP') {
// unconfigured Cognito — user needs to enter a code
const confirmResult = await foundation.auth.confirmSignUp(email, code)
if (confirmResult.isSignedIn) {
router.push('/dashboard') // already signed in — no trip back to the login form
} else {
router.push('/login') // check confirmResult.nextStep for why (e.g. MFA)
}
}confirmSignUp signs the user in for you: the password passed to signUp is held in
memory (never in storage) for 15 minutes so the confirmation code — which already proves
the user owns the address — completes the session instead of dead-ending at a login form.
The password is dropped as soon as it is used, and on signIn, logout, resetPassword,
or TTL expiry. Pass autoSignInAfterConfirm: false to createFoundation to turn it off.
Result types:
interface SignInResult {
isSignedIn: boolean
nextStep?: { signInStep: 'DONE' | 'CONFIRM_SIGN_UP' | string; [key: string]: unknown }
}
interface SignUpResult {
isSignUpComplete: boolean
userId?: string
nextStep?: { signUpStep: 'DONE' | 'CONFIRM_SIGN_UP' | string; [key: string]: unknown }
}
// confirmSignUp returns SignInResult: isSignedIn true means the session is ready.Other auth methods
foundation.auth.user // User | null
foundation.auth.isAuthenticated // boolean
foundation.auth.getToken() // current JWT
foundation.auth.login() // hosted login redirect
foundation.auth.logout()
foundation.auth.handleCallback() // call on your /callback route after OAuth redirect
foundation.auth.confirmSignUp(email, code) // -> SignInResult (auto signs in)
foundation.auth.resendSignUpCode(email)
foundation.auth.forgotPassword(email)
foundation.auth.resetPassword(email, code, newPassword)
foundation.auth.onChange(user => { ... })auth.resendSignUpCode is provider-aware: Cognito sends a new code; Auth0 hits the backend resend-verification endpoint to retrigger the email link.
Data (foundation.db)
Generic CRUD for any entity defined in your Foundation app:
const { items, nextCursor } = await foundation.db.list<Project>('projects', {
filters: { status: 'active' },
limit: 20,
orderBy: 'createdAt',
orderDir: 'desc'
})
// Run a named list template (from the entity's methods.list.templates).
// Sent on the wire as the reserved `query` param; pass the template's key
// fields via `filters`. If `filters` also contains a `query` key, the
// explicit `template` option wins.
const { items: matches } = await foundation.db.list<Company>('companies', {
template: 'by-domain',
filters: { domain: 'acme.com' },
limit: 1
})
// GET /api/v1/core/companies?domain=acme.com&query=by-domain&limit=1
const project = await foundation.db.get<Project>('projects', id)
const created = await foundation.db.create<Project>('projects', { name: 'New' })
const updated = await foundation.db.update<Project>('projects', id, { name: 'Renamed' })
await foundation.db.save<Project>('projects', data) // create-or-update
await foundation.db.delete('projects', id)Realtime (foundation.on)
Subscribe to entity-change events pushed over WebSocket:
const off = foundation.on('entity.changed', (event) => {
// event: EntityChangeEvent
// {
// changeType: 'created' | 'modified' | 'deleted'
// entityId?: string // entity record id
// resource?: string // entity resource name, e.g. 'companies'
// id?: string
// namespace?: string
// timestamp?: number
// }
})
off() // unsubscribeNo client-side configuration is needed — the SDK discovers the realtime endpoint from the auth token. The socket opens lazily on the first subscriber, is shared by all subscribers, heartbeats and reconnects automatically (server-side idle timeouts are normal), and closes on the last unsubscribe.
Backend prerequisite: events fire only for entities whose backend definition enables
them — websocket: { enabled: true } with scope namespace or user. If the backend has
no realtime support (or there is no WebSocket global, e.g. SSR), foundation.on logs a
single warning and the listener simply never fires — it does not throw.
Files (foundation.files)
// Upload (handles initiate + S3 PUT; sha256 is computed automatically)
const { id, name } = await foundation.files.upload({
name: file.name,
contentType: file.type,
file // ArrayBuffer | Blob | File
})
// Or get a presigned URL yourself. sha256 is optional here; if you pass it,
// it must be the BASE64-encoded digest (not hex) and S3 will verify the
// uploaded bytes against it.
const { signedUrl, signedData } = await foundation.files.initiate({ ... })
const file = await foundation.files.get(id)
const { items } = await foundation.files.list({ limit: 20 })
await foundation.files.delete(id)Integrations (foundation.integration)
// Get available connectors merged with the user's current connections
const all = await foundation.integration.all()
// [{ id, name, connections: [...], connected: true, connectionCount: 2, ... }]
// Or fetch them separately
const catalog = await foundation.integration.list()
const connections = await foundation.integration.connections()
// Status for a single source
const { connected, connections } = await foundation.integration.status('github-oauth')
// Connect (OAuth flow returns auth URL — open it in a popup or redirect)
const result = await foundation.integration.connect('github-oauth')
if (result.url) window.open(result.url, 'oauth')
await foundation.integration.disconnect('github-oauth', configurationId)Account (foundation.account)
const { user, account } = await foundation.account.get()
await foundation.account.update({ name: 'New Name' })
const usage = await foundation.account.usage()
await foundation.account.resendVerification()Pass type parameters for extended user/account shapes:
const { user, account } = await foundation.account.get<MyUser, MyAccount>()Billing (foundation.billing)
// List purchasable plans. plan.id is the Stripe price id checkout() takes;
// the account's current plan comes back disabled: true ("Current Plan").
const plans = await foundation.billing.plans()
// Start a subscription checkout (returns a plan-change portal flow instead
// when the account already has a plan). Redirect the user to the URL.
const { url } = await foundation.billing.checkout('price_pro_monthly')
window.location.href = url
// Open the Stripe customer portal
const { url: portalUrl } = await foundation.billing.portal()
// On your success page: the redirect can beat the Stripe webhook, so poll
// until the session is complete and paid before treating the plan as active.
const sessionId = new URLSearchParams(location.search).get('checkoutSessionId')!
const session = await foundation.billing.lookupSession(sessionId)
const paid = session.status === 'complete' && session.payment_status === 'paid'
// The account's subscriptions: status, card summary, renewal date, subjects
const subscriptions = await foundation.billing.subscriptions()checkout, portal, and subscriptions are account-owner only — other
members get a 403 with code: 'OWNER_REQUIRED'. plans and lookupSession
work for any authenticated user.
By default the backend derives redirect URLs from your app's origin
({origin}/callback/stripe, {origin}/plan, {origin}/settings/profile).
Apps with different routes override them — once at init, or per call:
const foundation = await createFoundation({
configUrl: '...',
billing: {
successUrl: 'https://app.example.com/billing/done', // after completed checkout
cancelUrl: 'https://app.example.com/pricing', // abandoned checkout
returnUrl: 'https://app.example.com/settings' // customer portal return
}
})
// Per-call options win over the init defaults, field by field
await foundation.billing.checkout('price_pro_monthly', {
successUrl: 'https://app.example.com/onboarding/complete'
})URLs must be absolute http(s). The backend appends
checkoutSessionId={CHECKOUT_SESSION_ID} to a custom successUrl unless you
already placed the {CHECKOUT_SESSION_ID} placeholder yourself.
OAuth (foundation.oauth)
For building consent screens when your app acts as an OAuth authorization server:
// Read OAuth params from URL (set by /oauth/authorize redirect)
const params = new URLSearchParams(location.search)
const clientId = params.get('client_id')!
// Get client info to display
const client = await foundation.oauth.getClient(clientId)
// { id, name, description, redirectUris, allowedScopes, ... }
// User clicks Allow → generate code, redirect back
const { code } = await foundation.oauth.authorizeConsent({
client_id: clientId,
redirect_uri: params.get('redirect_uri')!,
scope: params.get('scope')!,
state: params.get('state')!,
code_challenge: params.get('code_challenge')!,
code_challenge_method: params.get('code_challenge_method')!
})
const url = new URL(params.get('redirect_uri')!)
url.searchParams.set('code', code)
url.searchParams.set('state', params.get('state')!)
window.location.href = url.toString()Config (foundation.config)
The full config from your Foundation backend:
foundation.config.app // { id, name, version, environment }
foundation.config.features // feature flag definitions
foundation.config.plans // ConfigPlan[] — static plan defs (Foundation plan ids); empty pre-auth; billing.plans() has the Stripe prices
foundation.config.theme // { colors, dark, defaultColorScheme }
foundation.config.connectors // available integration connectors
foundation.config.resources // service endpoints
foundation.config.auth // auth provider config
foundation.config.raw // full config object for anything not surfaced aboveLogging (foundation.log)
foundation.log.info('Page loaded', { path: '/dashboard' })
foundation.log.warn('Slow query', { duration: 1234 })
foundation.log.error('Failed to save', { error })
foundation.log.event('button_clicked', { id: 'submit' })OpenAPI (foundation.openapi)
const spec = await foundation.openapi.get()Web Components
The SDK ships pre-built custom elements that handle complex flows. They work in any framework.
import 'foundation-sdk/components' // registers all custom elements<!-- OAuth integration connect modal — handles the popup, completion state, errors -->
<foundation-connect integration-id="github-oauth"></foundation-connect>The component reads the SDK instance from window.__foundation, so set it after creating:
const foundation = await createFoundation({ ... })
;(window as any).__foundation = foundationIf your framework warns about unknown elements, configure it to recognize foundation-* tags. For Vue:
// vite.config.ts
vue({
template: {
compilerOptions: {
isCustomElement: tag => tag.startsWith('foundation-')
}
}
})Browser Builds (Script Tag)
For apps without a bundler, two pre-bundled IIFE files are available:
<!-- Cognito auth bundled in -->
<script src="https://unpkg.com/foundation-sdk/dist/foundation-sdk.browser.global.js"></script>
<!-- Auth0 auth bundled in -->
<script src="https://unpkg.com/foundation-sdk/dist/foundation-sdk.browser.auth0.global.js"></script>
<script>
FoundationSDK.createFoundation({
configUrl: '...',
tenantId: '...',
appId: '...'
}).then(f => {
window.__foundation = f
})
</script>
<!-- Optional: web components -->
<script src="https://unpkg.com/foundation-sdk/dist/foundation-sdk.components.global.js"></script>Type Exports
import type {
Foundation,
FoundationConfig,
FullConfig,
User,
AuthClient,
AuthService,
AuthProvider,
SignInResult,
SignUpResult,
DbService,
FilesService,
FileMetadata,
AccountService,
BillingService,
BillingDefaults,
IntegrationService,
Integration,
IntegrationConnection,
IntegrationDetail,
OAuthService,
OAuthClient,
OAuthConsentParams,
OpenApiService,
LogService,
EntityChangeEvent
} from 'foundation-sdk'Dev Tip — local proxy
When developing against a remote Foundation backend, configure a Vite proxy and point baseUrl at it. This avoids CORS and gives you the network panel:
// vite.config.ts
server: {
proxy: {
'/api': { target: 'https://backend.your-app.com', changeOrigin: true, secure: true }
}
}const foundation = await createFoundation({
configUrl: '/api/v1/public/init',
baseUrl: '/api',
tenantId: '...',
appId: '...',
auth: cognitoAuth
})baseUrl is automatically ignored when /foundation-env.json is present, so this only takes effect in dev.
License
MIT
