Skip to main content
Machine-to-machine (M2M) authentication enables secure communication between services, APIs, and automated processes without user interaction. Stack Auth provides service accounts and API keys for M2M authentication.

Features

  • Service accounts for automated processes
  • API key authentication
  • Fine-grained permissions and scopes
  • Token-based authentication
  • No user interaction required
  • Audit logging for service account actions

Use Cases

M2M authentication is ideal for:
  • Backend services - Server-to-server API calls
  • Microservices - Service mesh authentication
  • CI/CD pipelines - Automated deployments and testing
  • Scheduled jobs - Cron jobs and background tasks
  • Data synchronization - ETL processes and data pipelines
  • Third-party integrations - External service communication
  • IoT devices - Device authentication and communication

Service Accounts

Service accounts are special accounts designed for automated processes:

Create Service Account

import { stackServerApp } from "@/stack";

const serviceAccount = await stackServerApp.createServiceAccount({
  name: "Data Sync Service",
  description: "Service account for daily data synchronization",
  permissions: ["users:read", "users:write"],
  scopes: ["api:full"]
});

// Returns
{
  id: "sa_1234567890",
  name: "Data Sync Service",
  apiKey: "sa_live_abcdef123456...",  // Store securely!
  createdAt: "2026-03-03T12:00:00Z"
}
API keys are only shown once during creation. Store them securely in a secrets manager or environment variables.

List Service Accounts

const serviceAccounts = await stackServerApp.listServiceAccounts();

serviceAccounts.forEach(account => {
  console.log(`${account.name}: ${account.id}`);
});

Update Service Account

await stackServerApp.updateServiceAccount("sa_1234567890", {
  name: "Updated Service Name",
  permissions: ["users:read"],  // Reduced permissions
});

Delete Service Account

await stackServerApp.deleteServiceAccount("sa_1234567890");

API Key Authentication

Server-Side Usage

Authenticate API requests using the service account API key:
import { StackServerApp } from "@stackframe/stack";

const serviceApp = new StackServerApp({
  apiKey: process.env.SERVICE_ACCOUNT_API_KEY,
  projectId: "your_project_id"
});

// Make authenticated requests
const users = await serviceApp.listUsers();

HTTP API Requests

Use the API key in HTTP requests:
GET /api/v1/users
Authorization: Bearer sa_live_abcdef123456...
Content-Type: application/json
Example with fetch:
const response = await fetch("https://api.stack-auth.com/api/v1/users", {
  headers: {
    "Authorization": `Bearer ${process.env.SERVICE_ACCOUNT_API_KEY}`,
    "Content-Type": "application/json"
  }
});

const users = await response.json();

Permissions and Scopes

Service accounts can be granted specific permissions:

Available Permissions

// Read user data
"users:read"

// Create and update users
"users:write"

// Delete users
"users:delete"

// Full user management
"users:*"

Grant Permissions

await stackServerApp.updateServiceAccount("sa_1234567890", {
  permissions: [
    "users:read",
    "users:write",
    "teams:read"
  ]
});

Token Exchange

Exchange service account API keys for short-lived access tokens:

Request Access Token

POST /api/v1/auth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "sa_1234567890",
  "client_secret": "sa_live_abcdef123456...",
  "scope": "users:read teams:read"
}
Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "users:read teams:read"
}

Use Access Token

const response = await fetch("https://api.stack-auth.com/api/v1/users", {
  headers: {
    "Authorization": `Bearer ${accessToken}`,
    "Content-Type": "application/json"
  }
});

Common Integration Patterns

Microservices Communication

// Service A calls Service B
import { StackServerApp } from "@stackframe/stack";

class ServiceClient {
  private stackApp: StackServerApp;

  constructor() {
    this.stackApp = new StackServerApp({
      apiKey: process.env.SERVICE_ACCOUNT_API_KEY,
      projectId: process.env.STACK_PROJECT_ID
    });
  }

  async callServiceB(userId: string) {
    // Authenticate using service account
    const user = await this.stackApp.getUser(userId);
    
    // Call Service B with authenticated context
    return await fetch(`https://service-b/api/process`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.SERVICE_ACCOUNT_API_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ user })
    });
  }
}

CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Stack Auth
        env:
          STACK_SERVICE_ACCOUNT_KEY: ${{ secrets.STACK_SERVICE_ACCOUNT_KEY }}
        run: |
          npm install @stackframe/stack
          
      - name: Create deployment user
        env:
          STACK_SERVICE_ACCOUNT_KEY: ${{ secrets.STACK_SERVICE_ACCOUNT_KEY }}
        run: |
          node scripts/create-deployment-user.js
create-deployment-user.js:
import { StackServerApp } from "@stackframe/stack";

const stackApp = new StackServerApp({
  apiKey: process.env.STACK_SERVICE_ACCOUNT_KEY,
  projectId: process.env.STACK_PROJECT_ID
});

const deploymentUser = await stackApp.createUser({
  email: `deploy-${Date.now()}@internal.company.com`,
  displayName: "Deployment Bot",
  metadata: {
    createdBy: "CI/CD",
    deploymentId: process.env.GITHUB_RUN_ID
  }
});

console.log("Created deployment user:", deploymentUser.id);

Scheduled Jobs

// cron/daily-sync.ts
import { StackServerApp } from "@stackframe/stack";
import { CronJob } from "cron";

const stackApp = new StackServerApp({
  apiKey: process.env.SERVICE_ACCOUNT_API_KEY,
  projectId: process.env.STACK_PROJECT_ID
});

const dailySync = new CronJob(
  "0 0 * * *",  // Run daily at midnight
  async () => {
    console.log("Starting daily user sync...");
    
    const users = await stackApp.listUsers();
    
    for (const user of users) {
      await syncUserToDataWarehouse(user);
    }
    
    console.log(`Synced ${users.length} users`);
  },
  null,
  true,
  "America/New_York"
);

async function syncUserToDataWarehouse(user) {
  // Sync logic here
}

IoT Device Authentication

// iot-device.ts
import { StackServerApp } from "@stackframe/stack";

class IoTDevice {
  private stackApp: StackServerApp;
  private deviceId: string;

  constructor(deviceId: string, apiKey: string) {
    this.deviceId = deviceId;
    this.stackApp = new StackServerApp({
      apiKey,
      projectId: process.env.STACK_PROJECT_ID
    });
  }

  async reportTelemetry(data: any) {
    // Store telemetry in user metadata
    await this.stackApp.updateUser(this.deviceId, {
      metadata: {
        lastTelemetry: data,
        lastSeen: new Date().toISOString()
      }
    });
  }

  async getConfiguration() {
    const device = await this.stackApp.getUser(this.deviceId);
    return device.metadata?.configuration ?? {};
  }
}

// Usage
const device = new IoTDevice(
  "device_123",
  process.env.IOT_SERVICE_ACCOUNT_KEY
);

await device.reportTelemetry({
  temperature: 72.5,
  humidity: 45.2,
  timestamp: Date.now()
});

API Key Rotation

Regularly rotate API keys for security:
// 1. Create new service account
const newAccount = await stackServerApp.createServiceAccount({
  name: serviceAccount.name + " (Rotated)",
  permissions: serviceAccount.permissions
});

// 2. Update environment variables with new key
process.env.SERVICE_ACCOUNT_API_KEY = newAccount.apiKey;

// 3. Deploy updated configuration
await deployConfiguration();

// 4. Delete old service account after verification
await stackServerApp.deleteServiceAccount(serviceAccount.id);

Audit Logging

Monitor service account activity:
const auditLogs = await stackServerApp.getAuditLogs({
  serviceAccountId: "sa_1234567890",
  startDate: new Date("2026-03-01"),
  endDate: new Date("2026-03-03")
});

auditLogs.forEach(log => {
  console.log(`${log.timestamp}: ${log.action} by ${log.serviceAccountId}`);
});

Security Best Practices

  1. Store keys securely - Use environment variables or secrets managers
  2. Principle of least privilege - Grant minimal required permissions
  3. Rotate keys regularly - Implement key rotation every 90 days
  4. Monitor usage - Set up alerts for unusual activity
  5. Use short-lived tokens - Exchange API keys for access tokens when possible
  6. Separate accounts - Use different service accounts for different services
  7. Revoke compromised keys - Immediately delete compromised service accounts
  8. Audit regularly - Review service account permissions quarterly

Environment Configuration

# .env
STACK_PROJECT_ID=your_project_id
STACK_SERVICE_ACCOUNT_KEY=sa_live_abcdef123456...

# Optional: Use different keys per environment
STACK_SERVICE_ACCOUNT_KEY_DEV=sa_dev_...
STACK_SERVICE_ACCOUNT_KEY_STAGING=sa_staging_...
STACK_SERVICE_ACCOUNT_KEY_PROD=sa_prod_...
Usage:
import { StackServerApp } from "@stackframe/stack";

const env = process.env.NODE_ENV || "development";
const apiKey = {
  development: process.env.STACK_SERVICE_ACCOUNT_KEY_DEV,
  staging: process.env.STACK_SERVICE_ACCOUNT_KEY_STAGING,
  production: process.env.STACK_SERVICE_ACCOUNT_KEY_PROD
}[env];

const stackApp = new StackServerApp({
  apiKey,
  projectId: process.env.STACK_PROJECT_ID
});

Error Handling

try {
  const users = await stackApp.listUsers();
} catch (error) {
  if (error.code === "INVALID_API_KEY") {
    console.error("Service account API key is invalid or expired");
  }
}

Limitations

  • Service accounts cannot sign in through the UI
  • Service accounts don’t have sessions or refresh tokens
  • Maximum 100 service accounts per project
  • API key length is fixed at 64 characters
  • Service accounts cannot have MFA enabled

Migration from API Keys

If you’re currently using project API keys, migrate to service accounts:
// Old approach (deprecated)
const stackApp = new StackServerApp({
  secretServerKey: process.env.STACK_SECRET_SERVER_KEY
});

// New approach (recommended)
const stackApp = new StackServerApp({
  apiKey: process.env.STACK_SERVICE_ACCOUNT_KEY,
  projectId: process.env.STACK_PROJECT_ID
});

API Reference

Key endpoints for M2M authentication:
  • POST /api/v1/service-accounts - Create service account
  • GET /api/v1/service-accounts - List service accounts
  • PATCH /api/v1/service-accounts/{id} - Update service account
  • DELETE /api/v1/service-accounts/{id} - Delete service account
  • POST /api/v1/auth/token - Exchange credentials for access token

Resources

Build docs developers (and LLMs) love