npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@scaffald/sdk

v0.3.0

Published

Official JavaScript/TypeScript SDK for the Scaffald API

Readme

@scaffald/sdk

Official JavaScript SDK for the Scaffald API

npm version License: MIT TypeScript Documentation

📚 Documentation | 📦 npm Package | 💻 GitHub

A complete, type-safe JavaScript/TypeScript SDK for integrating with the Scaffald job platform API.

Features

  • Full TypeScript Support - Auto-generated types from OpenAPI spec
  • Automatic Retries - Exponential backoff for failed requests (1s, 2s, 4s, 8s)
  • Rate Limit Handling - Tracks and respects API rate limits with callbacks
  • Request/Response Interceptors - Customize requests, responses, and error handling
  • Response Caching - Intelligent caching with TTL and cache invalidation
  • Request Deduplication - Prevent duplicate concurrent requests
  • API Key Management - Programmatically create, update, revoke, and monitor API keys
  • Zero Dependencies - Core SDK uses native Fetch API and Web Crypto
  • Universal - Works in Node.js 18+, modern browsers, and React Native 0.74+
  • React Integration - Hooks powered by React Query for optimal caching
  • OAuth 2.0 + PKCE - Secure authentication flow for user-facing apps
  • Webhook Verification - HMAC SHA-256 signature verification utilities
  • Tree-shakeable - Only import what you need
  • Bundle Optimized - Aggressive tree-shaking and code splitting

Installation

npm install @scaffald/sdk
# or
pnpm add @scaffald/sdk
# or
yarn add @scaffald/sdk

Quick Start

Server-side (Node.js) with API Key

import Scaffald from '@scaffald/sdk'

const client = new Scaffald({
  apiKey: 'sk_live_...' // Get from https://app.scaffald.com/api-keys
})

// List jobs
const jobs = await client.jobs.list({
  status: 'published',
  limit: 20,
  location: 'San Francisco',
  remoteOption: 'remote'
})

console.log(`Found ${jobs.pagination.total} jobs`)
jobs.data.forEach(job => {
  console.log(`- ${job.title} at ${job.location}`)
})

// Submit an application
const application = await client.applications.createQuick({
  jobId: jobs.data[0].id,
  currentLocation: 'San Francisco, CA',
  availableStartDate: '2025-03-01'
})

Client-side with OAuth

import Scaffald, { OAuthClient } from '@scaffald/sdk'

// Step 1: Get authorization URL
const oauth = new OAuthClient()
const { url, codeVerifier, state } = await oauth.getAuthorizationUrl({
  clientId: 'your_client_id',
  redirectUri: 'https://yourapp.com/callback',
  scope: ['read:jobs', 'write:applications']
})

// Store for later
sessionStorage.setItem('pkce_verifier', codeVerifier)
sessionStorage.setItem('oauth_state', state)

// Redirect user
window.location.href = url

// Step 2: In your callback handler
const code = new URL(window.location.href).searchParams.get('code')
const tokens = await oauth.exchangeCode({
  code,
  codeVerifier: sessionStorage.getItem('pkce_verifier'),
  clientId: 'your_client_id',
  redirectUri: 'https://yourapp.com/callback'
})

// Step 3: Create authenticated client
const client = new Scaffald({
  accessToken: tokens.access_token
})

React with Hooks

import { ScaffaldProvider, useJobs, useCreateQuickApplication } from '@scaffald/sdk/react'

function App() {
  return (
    <ScaffaldProvider config={{ apiKey: process.env.SCAFFALD_API_KEY }}>
      <JobsList />
    </ScaffaldProvider>
  )
}

function JobsList() {
  const { data: jobs, isLoading } = useJobs({
    status: 'published',
    limit: 20,
    remoteOption: 'remote'
  })

  const createApplication = useCreateQuickApplication()

  if (isLoading) return <div>Loading...</div>

  return (
    <div>
      {jobs?.data.map(job => (
        <div key={job.id}>
          <h3>{job.title}</h3>
          <p>{job.location || 'Remote'}</p>
          <button onClick={() => createApplication.mutate({
            jobId: job.id,
            currentLocation: 'San Francisco, CA'
          })}>
            Quick Apply
          </button>
        </div>
      ))}
    </div>
  )
}

API Reference

Jobs

// List jobs with filtering
const jobs = await client.jobs.list({
  status: 'published',      // Filter by status
  limit: 20,                // Results per page (max 100)
  offset: 0,                // Pagination offset
  organizationId: 'org_123', // Filter by organization
  location: 'San Francisco', // Filter by location (partial match)
  employmentType: 'full_time', // full_time | part_time | contract | temp | intern
  remoteOption: 'remote'    // on_site | hybrid | remote
})

// Get a specific job
const job = await client.jobs.retrieve('job_123')

// Get similar jobs
const similar = await client.jobs.similar('job_123', { limit: 5 })

// Get available filter options
const options = await client.jobs.filterOptions()
console.log(options.data.employmentTypes)
console.log(options.data.locations)
console.log(options.data.remoteOptions)

Applications

// Quick application
const quickApp = await client.applications.createQuick({
  jobId: 'job_123',
  currentLocation: 'San Francisco, CA',
  availableStartDate: '2025-03-01'
})

// Full application
const fullApp = await client.applications.createFull({
  jobId: 'job_123',
  currentLocation: 'San Francisco, CA',
  availableStartDate: '2025-03-01',
  coverLetter: 'I am excited to apply...',
  resumeUrl: 'https://example.com/resume.pdf',
  linkedinUrl: 'https://linkedin.com/in/johndoe',
  portfolioUrl: 'https://johndoe.com',
  yearsExperience: 5,
  salaryExpectationMinCents: 12000000, // $120,000
  salaryExpectationMaxCents: 15000000, // $150,000
  willingToRelocate: true,
  requiresSponsorship: false
})

// Get application status
const app = await client.applications.retrieve('app_123')

// Update application (only pending/reviewing status)
const updated = await client.applications.update('app_123', {
  coverLetter: 'Updated cover letter...',
  resumeUrl: 'https://example.com/new-resume.pdf'
})

// Withdraw application
await client.applications.withdraw('app_123', {
  reason: 'Accepted another offer'
})

Profiles (Rate-limited: 100 req/15 min)

// Get user profile by username
const profile = await client.profiles.getUser('johndoe')
console.log(profile.data.skills)
console.log(profile.data.certifications)

// Get organization profile
const org = await client.profiles.getOrganization('acme-corp')
console.log(`${org.data.name} has ${org.data.job_count} open jobs`)

// Get employer profile
const employer = await client.profiles.getEmployer('tech-startup')
console.log(`${employer.data.active_jobs_count} active positions`)

Industries

Industry lookup and categorization for job filtering.

// List all industries
const { data, total } = await client.industries.list()
console.log(`${total} industries available`)

data.forEach(industry => {
  console.log(`${industry.name} (${industry.slug})`)
})

// Get specific industry by slug
const industry = await client.industries.retrieve('technology')
console.log(industry?.name) // 'Technology'
console.log(industry?.description) // 'Technology and software development industry'

Organizations

Manage organizations, members, documents, and settings.

// Get organization details
const org = await client.organizations.retrieve('org_123')
console.log(`${org.name} - ${org.description}`)

// Get open jobs count
const { count } = await client.organizations.getOpenJobsCount('org_123')
console.log(`${count} open positions`)

// List organization members
const { data: members } = await client.organizations.listMembers('org_123', {
  search: 'john',
  roleNames: ['admin', 'member']
})

members.forEach(member => {
  console.log(`${member.user_profile.displayName} - ${member.role_name}`)
})

// Invite a new member
const { id, token } = await client.organizations.inviteMember('org_123', {
  email: '[email protected]',
  roleName: 'member',
  message: 'Welcome to the team!'
})

// Remove a member
await client.organizations.removeMember('org_123', 'user_456', {
  reason: 'Left the company'
})

// List organization documents
const { data: docs } = await client.organizations.listDocuments('org_123', {
  category: 'policies',
  limit: 10
})

// Get a specific document
const doc = await client.organizations.getDocument('org_123', 'doc_1')

// Create document upload session
const session = await client.organizations.createDocumentUploadSession('org_123', {
  name: 'Employee Handbook',
  fileName: 'handbook.pdf',
  mimeType: 'application/pdf',
  fileSize: 2048000,
  category: 'policies'
})
// Use session.uploadUrl to upload the file

// Create download URL for a document
const { downloadUrl } = await client.organizations.createDocumentDownloadUrl('org_123', 'doc_1')

// Get organization settings
const settings = await client.organizations.getSettings('org_123')
console.log(`Timezone: ${settings.timezone}`)
console.log(`MFA enforced: ${settings.enforce_mfa}`)

// Update organization settings
await client.organizations.updateSettings('org_123', {
  timezone: 'America/Los_Angeles',
  enforceMfa: true,
  sessionTimeoutMinutes: 30
})

Teams

Collaborative hiring teams for managing applications, members, and job assignments.

// List teams for an organization
const { teams } = await client.teams.list({
  organizationId: 'org_123',
  includeArchived: false
})

teams.forEach(team => {
  console.log(`${team.name} - ${team.purpose}`)
})

// Get team details
const { team } = await client.teams.retrieve('team_123')
console.log(`${team.name} has ${team.workloadStrategy} workload strategy`)

// Create a new team
const { team: newTeam } = await client.teams.create({
  organizationId: 'org_123',
  name: 'Engineering Hiring Team',
  purpose: 'Hire software engineers',
  visibility: 'organization',
  invitationPolicy: 'invite_only',
  autoAssignJobs: true,
  workloadStrategy: 'auto_balanced'
})

// Update a team
await client.teams.update('team_123', {
  name: 'Senior Engineering Hiring',
  description: 'Focus on senior-level positions',
  allowSelfJoin: true
})

// Archive a team
await client.teams.archive('team_123', {
  reason: 'Hiring season ended'
})

// List team members
const { members } = await client.teams.listMembers('team_123')
members.forEach(member => {
  console.log(`${member.user?.displayName} - ${member.role?.name}`)
})

// Add a team member
const { member } = await client.teams.addMember('team_123', {
  userId: 'user_456',
  roleKey: 'recruiter'
})

// Update a team member's role
await client.teams.updateMember('team_123', 'user_456', {
  roleKey: 'lead',
  status: 'active'
})

// Remove a team member
await client.teams.removeMember('team_123', 'user_456', {
  reason: 'Role ended'
})

// List team invitations
const { invitations } = await client.teams.listInvitations('team_123')
const pending = invitations.filter(inv => inv.status === 'pending')

// Invite a member
const { invitation } = await client.teams.inviteMember('team_123', {
  email: '[email protected]',
  roleKey: 'recruiter',
  message: 'Join our hiring team!'
})

// Cancel an invitation
await client.teams.cancelInvitation('team_123', 'inv_456')

// List job assignments
const { assignments } = await client.teams.listJobAssignments('team_123')
assignments.forEach(assignment => {
  console.log(`${assignment.job?.title} ${assignment.isPrimary ? '(Primary)' : ''}`)
})

// Assign a job to the team
const { assignment } = await client.teams.createJobAssignment('team_123', {
  jobId: 'job_789',
  isPrimary: true
})

// Remove a job assignment
await client.teams.deleteJobAssignment('team_123', 'assignment_999')

API Keys

Programmatically manage your organization's API keys for third-party integrations and SDK access.

⚠️ Security Note: API keys are sensitive credentials. Always store them securely and never commit them to version control.

// List all API keys
const keys = await client.apiKeys.list({ limit: 50, offset: 0 })

keys.data.forEach(key => {
  console.log(`${key.name}: ${key.key_prefix}... (${key.scopes.join(', ')})`)
  console.log(`  Status: ${key.is_active ? 'Active' : 'Inactive'}`)
  console.log(`  Last used: ${key.last_used_at || 'Never'}`)
})

// Create a new API key
const newKey = await client.apiKeys.create({
  name: 'Production Integration',
  scopes: ['read:jobs', 'read:applications'],
  environment: 'live',           // 'test' or 'live'
  rate_limit_tier: 'pro',        // 'free', 'pro', or 'enterprise'
  expires_at: '2025-12-31T23:59:59Z' // Optional expiration
})

// ⚠️ IMPORTANT: Save the full key immediately!
// This is the ONLY time you'll see the complete key
console.log('Save this key securely:', newKey.data.key)
console.log(newKey.warning) // "This key will only be shown once"

// Store in environment variable or secrets manager
// process.env.SCAFFALD_API_KEY = newKey.data.key

// Retrieve a specific API key (metadata only)
const key = await client.apiKeys.retrieve('key_abc123')
console.log(`${key.data.name} has scopes: ${key.data.scopes.join(', ')}`)

// Update API key
await client.apiKeys.update('key_abc123', {
  name: 'Production - Updated',
  scopes: ['read:jobs', 'write:jobs', 'read:applications'],
  is_active: true
})

// Get usage statistics
const usage = await client.apiKeys.getUsage('key_abc123', 30) // Last 30 days

console.log(`Total requests: ${usage.data.total_requests}`)
console.log(`Success rate: ${100 - parseFloat(usage.data.error_rate)}%`)
console.log(`Avg response time: ${usage.data.avg_response_time_ms}ms`)

// Breakdown by endpoint
usage.data.usage.forEach(req => {
  console.log(`${req.method} ${req.endpoint}: ${req.status_code} (${req.response_time_ms}ms)`)
})

// Revoke an API key (permanent soft delete)
await client.apiKeys.revoke('key_abc123')
console.log('API key has been permanently revoked')

Available Scopes:

  • read:jobs - View published job listings
  • write:jobs - Create and manage job postings
  • read:applications - View job applications
  • write:applications - Submit and manage applications
  • read:profiles - Access user profiles
  • write:profiles - Update user profiles
  • read:organizations - View organization data
  • write:organizations - Manage organization settings

Rate Limit Tiers:

  • free - 100 requests per 15 minutes
  • pro - 1,000 requests per 15 minutes
  • enterprise - 10,000 requests per 15 minutes

Security Best Practices:

  1. Never expose API keys in client-side code - Only use from server-side
  2. Use test keys for development - sk_test_... keys for testing, sk_live_... for production
  3. Rotate keys regularly - Create new keys and revoke old ones periodically
  4. Use minimal scopes - Only grant permissions your integration needs
  5. Monitor usage - Check usage statistics to detect anomalies
  6. Set expiration dates - Use short-lived keys when possible

Rate Limiting

The SDK automatically tracks rate limits and provides helpers:

// Get current rate limit info
const info = client.getRateLimitInfo()
console.log(`${info.remaining}/${info.limit} requests remaining`)
console.log(`Resets at: ${new Date(info.reset * 1000)}`)

// Subscribe to rate limit updates
const unsubscribe = client.onRateLimitUpdate((info) => {
  if (info.remaining < 10) {
    console.warn('Rate limit approaching!')
  }
})

// Check if rate limit is approaching (< 20% remaining)
if (client.isRateLimitApproaching()) {
  console.warn('Slow down!')
}

// Get seconds until reset
const seconds = client.getSecondsUntilRateLimitReset()
console.log(`Rate limit resets in ${seconds} seconds`)

Error Handling

import {
  ScaffaldError,
  RateLimitError,
  AuthenticationError,
  AuthorizationError,
  NotFoundError,
  ValidationError,
  ServerError,
  NetworkError
} from '@scaffald/sdk'

try {
  const job = await client.jobs.retrieve('invalid_id')
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(`Rate limited. Retry after ${error.retryAfter} seconds`)
    console.log(`Limit: ${error.limit}, Remaining: ${error.remaining}`)
  } else if (error instanceof AuthenticationError) {
    console.log('Invalid API key or access token')
  } else if (error instanceof NotFoundError) {
    console.log('Job not found')
  } else if (error instanceof ValidationError) {
    console.log('Validation errors:', error.errors)
  } else if (error instanceof ServerError) {
    console.log(`Server error ${error.statusCode}: ${error.message}`)
  } else if (error instanceof NetworkError) {
    console.log('Network request failed or timed out')
  } else if (error instanceof ScaffaldError) {
    console.log(`API error: ${error.message}`)
  }
}

Webhooks

Verify webhook signatures to ensure requests come from Scaffald:

import { verifyWebhookSignature, parseWebhook } from '@scaffald/sdk'
import type { WebhookPayload } from '@scaffald/sdk'

// Express example
app.post('/webhooks/scaffald', async (req, res) => {
  const signature = req.headers['x-webhook-signature']

  // Option 1: Verify and parse in one step
  const payload = await parseWebhook(
    req.body,
    signature,
    process.env.SCAFFALD_WEBHOOK_SECRET
  )

  if (!payload) {
    return res.status(401).send('Invalid signature')
  }

  // Option 2: Verify separately
  const isValid = await verifyWebhookSignature(
    req.body,
    signature,
    process.env.SCAFFALD_WEBHOOK_SECRET
  )

  // Process webhook
  switch (payload.event) {
    case 'application.created':
      console.log('New application:', payload.data)
      break
    case 'application.updated':
      console.log('Application updated:', payload.data)
      break
    case 'application.withdrawn':
      console.log('Application withdrawn:', payload.data)
      break
  }

  res.status(200).send('OK')
})

OAuth 2.0 Integration

Complete OAuth flow with PKCE for secure client-side authentication:

import { OAuthClient, generateCodeVerifier, generateCodeChallenge } from '@scaffald/sdk'

const oauth = new OAuthClient('https://api.scaffald.com')

// Get authorization URL
const { url, codeVerifier, state } = await oauth.getAuthorizationUrl({
  clientId: 'your_client_id',
  redirectUri: 'https://yourapp.com/callback',
  scope: ['read:jobs', 'write:applications', 'read:profile']
})

// Store PKCE verifier securely
sessionStorage.setItem('pkce_verifier', codeVerifier)
sessionStorage.setItem('oauth_state', state)

// Redirect user
window.location.href = url

// In your callback handler
const code = new URL(window.location.href).searchParams.get('code')
const returnedState = new URL(window.location.href).searchParams.get('state')

// Verify state to prevent CSRF
if (returnedState !== sessionStorage.getItem('oauth_state')) {
  throw new Error('Invalid state parameter')
}

// Exchange code for tokens
const tokens = await oauth.exchangeCode({
  code,
  codeVerifier: sessionStorage.getItem('pkce_verifier'),
  clientId: 'your_client_id',
  redirectUri: 'https://yourapp.com/callback'
})

// Store tokens
localStorage.setItem('access_token', tokens.access_token)
localStorage.setItem('refresh_token', tokens.refresh_token)

// Refresh token when expired
const newTokens = await oauth.refreshToken(
  localStorage.getItem('refresh_token'),
  'your_client_id'
)

// Revoke token on logout
await oauth.revokeToken(
  localStorage.getItem('access_token'),
  'access_token',
  'your_client_id'
)

React Hooks

Complete list of available hooks:

Jobs Hooks

  • useJobs(params?) - List jobs with filtering
  • useJob(id) - Get single job by ID
  • useSimilarJobs(id, params?) - Get similar jobs
  • useJobFilterOptions() - Get available filter values

Applications Hooks

  • useCreateQuickApplication() - Submit quick application mutation
  • useCreateFullApplication() - Submit full application mutation
  • useApplication(id) - Get application by ID
  • useUpdateApplication(id) - Update application mutation
  • useWithdrawApplication(id) - Withdraw application mutation

Profiles Hooks

  • useUserProfile(username) - Get user profile
  • useOrganization(slug) - Get organization profile
  • useEmployer(slug) - Get employer profile

Industries Hooks

  • useIndustries() - List all industries
  • useIndustry(slug) - Get industry by slug

Organizations Hooks

  • useOrganization(id) - Get organization by ID
  • useOrganizationJobsCount(id) - Get open jobs count
  • useOrganizationMembers(id, params?) - List organization members
  • useInviteOrganizationMember() - Invite member mutation
  • useOrganizationDocuments(id, params?) - List documents
  • useCreateDocumentUpload() - Create upload session mutation
  • useOrganizationSettings(id) - Get organization settings
  • useUpdateOrganizationSettings() - Update settings mutation

Teams Hooks

  • useTeams(params?) - List teams
  • useTeam(id) - Get team by ID
  • useCreateTeam() - Create team mutation
  • useUpdateTeam() - Update team mutation
  • useArchiveTeam() - Archive team mutation
  • useTeamMembers(teamId) - List team members
  • useAddTeamMember() - Add member mutation
  • useUpdateTeamMember() - Update member mutation
  • useRemoveTeamMember() - Remove member mutation
  • useTeamInvitations(teamId) - List team invitations
  • useInviteTeamMember() - Invite member mutation
  • useCancelTeamInvitation() - Cancel invitation mutation
  • useTeamJobAssignments(teamId) - List job assignments
  • useCreateTeamJobAssignment() - Create assignment mutation
  • useDeleteTeamJobAssignment() - Delete assignment mutation

API Keys Hooks

  • useAPIKeys(params?) - List all API keys
  • useAPIKey(id) - Get single API key by ID
  • useCreateAPIKey() - Create new API key mutation
  • useUpdateAPIKey() - Update API key mutation
  • useRevokeAPIKey() - Revoke API key mutation
  • useAPIKeyUsage(id, days?) - Get usage statistics

All hooks automatically handle:

  • Loading states
  • Error handling
  • Caching with React Query
  • Cache invalidation on mutations
  • Optimistic updates

Configuration

const client = new Scaffald({
  // Required: API key or access token
  apiKey: 'sk_live_...',        // For server-side
  // OR
  accessToken: 'oauth_token',   // For client-side

  // Optional
  baseUrl: 'https://api.scaffald.com', // Custom API URL
  maxRetries: 3,                // Number of retry attempts (0-10)
  timeout: 60000,               // Request timeout in ms (1s-5min)
  headers: {                    // Additional headers
    'X-Custom-Header': 'value'
  }
})

Platform Support

  • Node.js: 18+
  • Browsers: Modern browsers with ES2020+ support
  • React Native: 0.74+
  • Deno: Full support via npm specifiers

Examples

See the examples/ directory for complete working examples:

  • Node.js - Server-side API usage with webhook handling
  • Browser - Vanilla JavaScript browser app
  • React - React app with hooks and React Query

Bundle Size

  • Core SDK: ~11 KB (minified + gzipped)
  • React package: ~12 KB (minified + gzipped)
  • OAuth standalone: ~1.8 KB (minified + gzipped)
  • Webhooks standalone: ~0.7 KB (minified + gzipped)
  • Zero runtime dependencies (except React Query for React package)

Analyze bundle sizes after building:

pnpm build:analyze

Advanced Features

The SDK includes powerful features for optimizing API usage and customizing behavior:

Request/Response Interceptors

Add custom headers, log requests, or transform responses:

const client = new Scaffald({ apiKey: 'sk_live_...' })

// Add request interceptor
client.getInterceptors().addRequestInterceptor(async (url, init) => {
  console.log('Request:', url)
  return { url, init }
})

// Add response interceptor
client.getInterceptors().addResponseInterceptor(async (response) => {
  console.log('Response:', response.status)
  return response
})

Response Caching

Enable intelligent caching for GET requests:

const client = new Scaffald({
  apiKey: 'sk_live_...',
  cache: {
    enabled: true,
    defaultTtl: 5 * 60 * 1000, // 5 minutes
    maxSize: 100,
  },
})

// First call hits the API
const jobs = await client.jobs.list()

// Second call uses cache (within TTL)
const cachedJobs = await client.jobs.list() // Instant!

// Clear cache after mutations
await client.jobs.create(/* ... */)
client.getCache().invalidate(/jobs/)

Request Deduplication

Automatically prevent duplicate concurrent requests:

// These three simultaneous calls result in only ONE HTTP request
const [job1, job2, job3] = await Promise.all([
  client.jobs.retrieve('job_123'),
  client.jobs.retrieve('job_123'),
  client.jobs.retrieve('job_123'),
])

Code Splitting

Import only what you need for optimal bundle size:

// Full SDK
import Scaffald from '@scaffald/sdk'

// OAuth only (smaller bundle)
import { OAuthClient } from '@scaffald/sdk/oauth'

// Webhooks only (smaller bundle)
import { verifyWebhookSignature } from '@scaffald/sdk/webhooks'

📖 Complete Advanced Features Guide

Architecture

Scaffald uses a hybrid architecture combining REST SDK and tRPC:

  • SDK (REST API): User-facing data operations, public endpoints
  • tRPC: Complex workflows, file operations, admin tools, provider integrations

See ARCHITECTURE.md for detailed architectural decisions and guidelines.

When to Use SDK vs tRPC

Use SDK for:

  • User-facing CRUD operations
  • Read-heavy operations
  • Public API candidates
  • Standard HTTP patterns

Use tRPC for:

  • File uploads/downloads
  • External provider integration (Stripe, etc.)
  • Admin-only operations
  • Compliance workflows

See SDK_MIGRATION_GUIDELINES.md for migration decision trees and checklists.

Testing

The SDK has comprehensive test coverage using Vitest and MSW (Mock Service Worker).

Running Tests

# Run all tests
pnpm test

# Run tests for specific resource
pnpm test teams

# Run tests in watch mode
pnpm test:watch

# Generate coverage report
pnpm test:coverage

Test Coverage

  • 93% resource coverage (28/30 resources)
  • 31 test files including infrastructure tests
  • Comprehensive scenarios: happy paths, edge cases, error handling
  • All error codes covered: 401, 404, 429, 500

Writing Tests

See existing test files in src/__tests__/ for patterns. Each resource test should cover:

  • All methods (list, get, create, update, delete)
  • Happy paths with realistic data
  • Edge cases (empty, null, missing)
  • Validation errors (400)
  • Auth errors (401)
  • Not found (404)
  • Rate limiting (429)
  • Server errors (500)

See SDK_MIGRATION_GUIDELINES.md#testing-patterns for detailed testing patterns.

API Coverage

The SDK covers 31 resources across:

  • Core Data: jobs, applications, profiles, user-profiles, organizations, teams
  • Profile Extensions: skills, experience, education, certifications, portfolio, reviews, projects
  • Social: connections, follows, engagement, profile-views
  • Supporting: prerequisites, profile-completion, employers, onet, inquiries, notifications

See API_COVERAGE_MATRIX.md for complete coverage details.

Development

# Install dependencies
pnpm install

# Type check
pnpm typecheck

# Build
pnpm build

# Run tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Generate types from OpenAPI spec (optional)
pnpm generate:types

Type Generation

The SDK uses hand-crafted TypeScript types that are kept in sync with the API. Optionally, you can generate types from the live OpenAPI specification:

# Generate from local Supabase instance
pnpm supa start  # Start Supabase first
pnpm generate:types

# Generate from production
SUPABASE_URL=https://your-project.supabase.co pnpm generate:types

# Generate from custom URL
OPENAPI_URL=https://api.scaffald.com/openapi.json pnpm generate:types

See Type Generation Guide for detailed information.

License

MIT © Scaffald Team

Support