API SDK Contracts Specification
This document serves as both a specification and implementation guide for the SDK contract system. The infrastructure (GitHub App, secrets, Terraform) is already configured. This document explains the architecture, what was set up, and how to implement the remaining pieces.
Implementation Status
| Component | Status | Details |
|---|---|---|
| GitHub App (eli-sdk-bot) | ✅ Complete | App ID: 2659740, installed on all repos |
| Terraform Secrets | ✅ Complete | SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY deployed |
| HAE OpenAPI Spec | ✅ Exists | FastAPI auto-generates at /openapi.json |
| Backend OpenAPI Spec | ✅ Exists | NestJS Swagger at /docs-json |
| SDK Generation Workflows | ⏳ Pending | GitHub Actions workflows to be created |
| SDK Packages | ⏳ Pending | @eli/hae-sdk, @eli/backend-sdk to be published |
| Consumer Integration | ⏳ Pending | Backend and Mobile to import SDKs |
Executive Summary
This system creates type-safe contracts between Eli Health services. When fully implemented, the mobile app will not compile if it uses API calls that don't match the backend specification, and the backend will not compile if it uses HAE API calls incorrectly.
Infrastructure Already Configured
The following infrastructure was set up on January 15, 2025:
GitHub App: eli-sdk-bot
A GitHub App was created to handle cross-repository package access. This approach was chosen over Personal Access Tokens (PATs) because:
- Not tied to any individual - survives employee turnover
- Generates short-lived tokens automatically (more secure)
- Better audit trail
- Organization-owned, not personal
How it was created:
- Navigate to:
https://github.com/organizations/eli-health/settings/apps/new - Configure:
- Name:
eli-sdk-bot - Homepage URL:
https://eli.health - Webhook: Unchecked (not needed)
- Repository permissions:
- Contents: Read-only
- Metadata: Read-only (mandatory)
- Packages: Read and write
- Where can this app be installed?: Only on this account
- Name:
- After creation:
- Generated a private key (downloaded as
.pemfile) - Noted the App ID:
2659740 - Installed the app on eli-health organization (all repositories)
- Generated a private key (downloaded as
Terraform Secrets Configuration
The GitHub App credentials are stored as GitHub Actions secrets via Terraform.
Files created/modified in eli-devops:
eli-devops/github/variables.tf - Added:
variable "sdk_bot_app_id" {
description = "GitHub App ID for Eli SDK Bot"
type = string
default = "2659740"
}
variable "sdk_bot_private_key" {
description = "Private key (.pem) for Eli SDK Bot GitHub App"
type = string
sensitive = true
}
eli-devops/github/secrets-sdk.tf - Created:
# SDK Bot Secrets for eli_hae_api
resource "github_actions_secret" "eli_hae_api_sdk_bot_app_id" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_APP_ID"
plaintext_value = var.sdk_bot_app_id
}
resource "github_actions_secret" "eli_hae_api_sdk_bot_private_key" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_PRIVATE_KEY"
plaintext_value = var.sdk_bot_private_key
}
# Same pattern repeated for eli-backend-api and eli-app
eli-devops/github/tfvars/github.tfvars - Added (sensitive, gitignored):
sdk_bot_app_id = "2659740"
sdk_bot_private_key = <<-EOT
-----BEGIN RSA PRIVATE KEY-----
... (private key content) ...
-----END RSA PRIVATE KEY-----
EOT
Secrets deployed to repositories:
| Repository | Secrets |
|---|---|
eli_hae_api | SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY |
eli-backend-api | SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY |
eli-app | SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY |
To verify secrets are deployed, check: https://github.com/eli-health/{repo}/settings/secrets/actions
Problem Statement
Current Issues
- Communication Gaps: API changes in one service break consumers without warning
- Manual Type Maintenance: Types are manually duplicated across services
- Runtime Errors: Type mismatches are only discovered at runtime
- Integration Friction: Teams must coordinate verbally for API changes
Real Example: Why This System Matters
A backend field was removed that the mobile app was still using. The current system gave no warning - this only surfaced as a runtime error in production. With SDK contracts, the mobile app build would have failed immediately, giving instant feedback before deployment and preventing the production issue entirely.
Current State Analysis
HAE API (Python/FastAPI)
Good News: FastAPI already generates OpenAPI 3.0+ specs automatically.
| Component | Status | Details |
|---|---|---|
| OpenAPI Spec | ✅ Auto-generated | Available at /openapi.json |
| Swagger UI | ✅ Available | /docs endpoint |
| Pydantic Models | ✅ Complete | All requests/responses typed |
| Endpoints | 21 endpoints | Fully documented |
Key Files (eli_hae_api repo):
- Entry point:
app.py - Models:
api/models/ - Version:
v1.0.120(from.versionfile)
Backend API (TypeScript/NestJS)
Good News: NestJS Swagger is already integrated.
| Component | Status | Details |
|---|---|---|
| OpenAPI Spec | ✅ Auto-generated | Available at /docs-json |
| Swagger UI | ✅ Available | /docs endpoint (disabled in prod) |
| Swagger Decorators | ✅ Present | Controllers use @ApiTags, @ApiResponse, etc. |
| Controllers | 20+ controllers | Comprehensive API surface |
Key Files (eli-backend-api repo):
- Entry point:
src/main.ts - Swagger Config: Lines 39-48 in
main.ts - Models:
src/models/
Mobile App (React Native)
Current State: Manual type definitions, no SDK.
| Component | Status | Details |
|---|---|---|
| API Client | ✅ Generic typed | get<T>(), post<T>() methods |
| Service Layer | ✅ Well-structured | /src/services/*.ts |
| Types | ⚠️ Manual | Defined in /src/common/models/ |
| SDK | ❌ None | Types manually maintained |
Key Files (eli-app repo):
- API Client:
src/shared/api.client.ts - Common Models:
src/common/models/ - Services:
src/services/
Proposed Solution
Architecture Overview
SDK Generation Tool Selection
After evaluating available options, the recommended approach is:
| Tool | Pros | Cons | Recommendation |
|---|---|---|---|
| openapi-typescript | Fast, lightweight, TypeScript-native, excellent tree-shaking | Types only (no HTTP client) | ✅ For types |
| Hey API | Purpose-built for TypeScript, FastAPI recommended | Newer tool | ✅ For full client |
| OpenAPI Generator | Supports 50+ languages | Generic output, requires refinement | Consider for multi-language |
| Fern/Speakeasy | Production-ready output, automatic publishing | Commercial (cost) | Future consideration |
Recommended Stack:
- Types Generation:
@hey-api/openapi-ts(FastAPI recommended) - Package Publishing: GitHub Packages (npm registry)
- CI Integration: GitHub Actions (existing infrastructure)
SDK Package Structure
@eli/hae-sdk/
├── package.json
├── src/
│ ├── index.ts # Main exports
│ ├── types/ # Generated types
│ │ ├── models.ts # Request/response models
│ │ └── operations.ts # API operations
│ └── client/ # Generated HTTP client
│ ├── index.ts
│ └── services/
│ ├── ImageAnalysisService.ts
│ ├── MetricsService.ts
│ └── ...
└── openapi.json # Source spec (for reference)
@eli/backend-sdk/
├── package.json
├── src/
│ ├── index.ts
│ ├── types/
│ │ ├── models.ts # IUser, IReading, IRecord, etc.
│ │ └── operations.ts
│ └── client/
│ └── services/
│ ├── UsersService.ts
│ ├── ReadingsService.ts
│ ├── HealthTagService.ts
│ └── ...
└── openapi.json
Implementation Plan
Phase 1: HAE SDK for Backend
Objective: Generate TypeScript SDK from HAE OpenAPI spec for Backend consumption.
Step 1.1: Enhance HAE OpenAPI Configuration
Improve operation IDs and add custom schema metadata:
# eli_hae_api/app.py
from fastapi import FastAPI
from fastapi.routing import APIRoute
def custom_generate_unique_id(route: APIRoute) -> str:
"""Generate clean operation IDs like 'analyzeImage' instead of 'analyze_image_images_analysis_post'"""
return route.name
app = FastAPI(
title="Eli HAE API",
description="Health Analysis Engine - Image processing and ML inference",
version="1.0.120",
generate_unique_id_function=custom_generate_unique_id,
)
Step 1.2: Create SDK Generation Script
# eli_hae_api/scripts/generate-sdk.sh
#!/bin/bash
set -e
# Configuration
OUTPUT_DIR="sdk-output"
PACKAGE_NAME="@eli/hae-sdk"
VERSION=$(cat .version)
# 1. Ensure HAE is running (or use local openapi.json)
if [ -f "openapi.json" ]; then
SPEC_FILE="openapi.json"
else
echo "Extracting OpenAPI spec from running instance..."
curl -s http://localhost:8000/openapi.json > openapi.json
SPEC_FILE="openapi.json"
fi
# 2. Generate TypeScript SDK
npx @hey-api/openapi-ts \
-i $SPEC_FILE \
-o $OUTPUT_DIR/src \
-c @hey-api/client-fetch
# 3. Create package.json
cat > $OUTPUT_DIR/package.json << EOF
{
"name": "$PACKAGE_NAME",
"version": "$VERSION",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@hey-api/client-fetch": "^0.2.0"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
EOF
# 4. Create tsconfig.json
cat > $OUTPUT_DIR/tsconfig.json << EOF
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
EOF
echo "SDK generated in $OUTPUT_DIR"
Step 1.3: GitHub Actions Workflow for HAE SDK
# eli_hae_api/.github/workflows/generate-sdk.yaml
name: Generate and Publish HAE SDK
on:
push:
branches: [development, staging, main]
paths:
- 'api/**'
- 'app.py'
- '.version'
workflow_dispatch:
jobs:
generate-sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install -r requirements-prod.txt
- name: Extract OpenAPI spec
run: |
python -c "
from app import app
import json
with open('openapi.json', 'w') as f:
json.dump(app.openapi(), f, indent=2)
"
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://npm.pkg.github.com'
- name: Generate SDK
run: |
npx @hey-api/openapi-ts \
-i openapi.json \
-o sdk/src \
-c @hey-api/client-fetch
- name: Setup SDK package
run: |
VERSION=$(cat .version)
cd sdk
npm init -y
npm pkg set name="@eli/hae-sdk"
npm pkg set version="$VERSION"
npm pkg set main="dist/index.js"
npm pkg set types="dist/index.d.ts"
npm install typescript @hey-api/client-fetch
npx tsc --init --declaration --outDir dist
npm run build
- name: Publish to GitHub Packages
run: |
cd sdk
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Backend SDK update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.HAE_SDK_UPDATE_PAT }}
repository: eli-health/eli-backend-api
event-type: hae-sdk-updated
client-payload: '{"version": "${{ env.VERSION }}"}'
Step 1.4: Backend Integration
Update eli-backend-api/package.json:
{
"dependencies": {
"@eli/hae-sdk": "^1.0.120"
}
}
Usage in backend:
// eli-backend-api/src/modules/image-analysis/image-analysis.service.ts
import { ImageAnalysisService, CortisolAnalysisPayload } from '@eli/hae-sdk';
@Injectable()
export class ImageAnalysisServiceImpl {
async analyzeImage(payload: CortisolAnalysisPayload) {
// TypeScript will error if payload doesn't match HAE's expected structure
const result = await ImageAnalysisService.imageAnalysis({ body: payload });
return result;
}
}
Phase 2: Backend SDK for Mobile App
Objective: Generate TypeScript SDK from Backend OpenAPI spec for Mobile consumption.
Step 2.1: Enhance Backend OpenAPI Configuration
// eli-backend-api/src/main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle('Eli Backend API')
.setDescription('Core API for Eli Health mobile application')
.setVersion(process.env.npm_package_version || '1.0.0')
.addBearerAuth()
.addTag('users', 'User management and authentication')
.addTag('readings', 'Test strip readings and analysis')
.addTag('records', 'Health records management')
.addTag('health-tags', 'Health tagging system')
.build();
const document = SwaggerModule.createDocument(app, config);
// Export OpenAPI spec for SDK generation
if (process.env.EXPORT_OPENAPI === 'true') {
const fs = require('fs');
fs.writeFileSync('openapi.json', JSON.stringify(document, null, 2));
}
Step 2.2: GitHub Actions Workflow for Backend SDK
# eli-backend-api/.github/workflows/generate-sdk.yaml
name: Generate and Publish Backend SDK
on:
push:
branches: [development, staging, main]
paths:
- 'src/modules/**/*.controller.ts'
- 'src/models/**'
- 'package.json'
repository_dispatch:
types: [hae-sdk-updated]
workflow_dispatch:
jobs:
generate-sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://npm.pkg.github.com'
- name: Install dependencies
run: npm ci
- name: Extract OpenAPI spec
run: |
EXPORT_OPENAPI=true npm run build
# Or use a dedicated script
npm run export:openapi
- name: Generate SDK
run: |
npx @hey-api/openapi-ts \
-i openapi.json \
-o sdk/src \
-c @hey-api/client-fetch
- name: Setup and publish SDK
run: |
VERSION=$(node -p "require('./package.json').version")
cd sdk
npm init -y
npm pkg set name="@eli/backend-sdk"
npm pkg set version="$VERSION"
npm install typescript @hey-api/client-fetch
npx tsc --init --declaration --outDir dist
npm run build
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Step 2.3: Mobile App Integration
Update eli-app/package.json:
{
"dependencies": {
"@eli/backend-sdk": "^1.0.0"
}
}
Migration example in mobile:
// Before (manual types)
import { IUser } from '@src/common/models';
import apiClient from '@shared/api.client';
export function getCurrentUser(): Promise<IUser> {
return apiClient.get<IUser>('/users/current');
}
// After (SDK types - compile-time safe)
import { UsersService, IUser } from '@eli/backend-sdk';
export function getCurrentUser(): Promise<IUser> {
return UsersService.getCurrentUser();
}
Phase 3: CI/CD Integration
Workflow Dependency Chain
Version Strategy: Automatic Schema Change Detection
The SDK version increments automatically when the OpenAPI schema changes. This ensures every schema modification produces a new version.
How It Works:
Implementation:
# In CI workflow - detect schema changes
CURRENT_HASH=$(sha256sum openapi.json | cut -d' ' -f1)
PREVIOUS_HASH=$(cat .openapi-hash 2>/dev/null || echo "none")
if [ "$CURRENT_HASH" != "$PREVIOUS_HASH" ]; then
# Schema changed - bump version and publish
npm version patch
echo "$CURRENT_HASH" > .openapi-hash
npm publish
else
echo "No schema changes detected, skipping publish"
fi
Version Format:
| Change Type | Version Bump | Example |
|---|---|---|
| New endpoint added | Minor | 1.0.0 → 1.1.0 |
| Field added/removed | Patch | 1.1.0 → 1.1.1 |
| Breaking change | Major | 1.1.1 → 2.0.0 |
Breaking Change Detection:
The CI can automatically detect breaking changes by comparing OpenAPI specs:
# Use openapi-diff to detect breaking changes
npx openapi-diff previous-openapi.json openapi.json --breaking-only
if [ $? -eq 1 ]; then
npm version major # Breaking change detected
else
npm version patch # Non-breaking change
fi
Consumers always use latest:
{
"@eli/backend-sdk": "latest"
}
When a new version is published, consumer CI pipelines automatically pick it up and will fail to build if there are incompatibilities.
Breaking Change Detection
Compile-Time Enforcement
When a breaking change is introduced:
Example Error Output
# Mobile app build output after breaking Backend change
src/services/user.ts:42:15 - error TS2339: Property 'legacyField' does not exist on type 'IUser'.
42 return user.legacyField;
~~~~~~~~~~~
Found 1 error in src/services/user.ts:42
GitHub Packages: How It Works
GitHub Packages is a private npm registry tied to the eli-health GitHub organization. Only repos/users in the org can access it.
Flow 1: HAE (Python) → Backend (TypeScript)
Flow 2: Backend (TypeScript) → Mobile (React Native)
Key Concepts
| Question | Answer |
|---|---|
| Is it private? | Yes - only eli-health org members can see/download |
| Where does the SDK live? | npm.pkg.github.com/@eli/hae-sdk |
| Where does Docker live? | GCP Artifact Registry (separate from SDK) |
| Who can publish? | The repo that owns the package (via GITHUB_TOKEN) |
| Who can read? | Any repo with a valid token (PAT or GITHUB_TOKEN with permissions) |
Authentication: GitHub App (eli-sdk-bot)
Instead of using Personal Access Tokens (tied to individuals), we use a GitHub App that belongs to the organization.
App Details:
| Property | Value |
|---|---|
| Name | eli-sdk-bot |
| App ID | 2659740 |
| Permissions | Contents (read), Metadata (read), Packages (read/write) |
| Installed on | All eli-health repositories |
| Management URL | https://github.com/organizations/eli-health/settings/apps/eli-sdk-bot |
Key Rotation: If the private key needs to be rotated:
- Go to the app management URL above
- Scroll to "Private keys" section
- Click "Generate a private key" (downloads new
.pem) - Update
eli-devops/github/tfvars/github.tfvarswith new key - Run
terraform apply -var-file=tfvars/github.tfvars - Delete the old private key from the GitHub App page
Terraform Configuration (Already Applied)
The secrets are configured in eli-devops/github/secrets-sdk.tf:
# eli_hae_api - publishes @eli/hae-sdk
resource "github_actions_secret" "eli_hae_api_sdk_bot_app_id" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_APP_ID"
plaintext_value = var.sdk_bot_app_id
}
resource "github_actions_secret" "eli_hae_api_sdk_bot_private_key" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_PRIVATE_KEY"
plaintext_value = var.sdk_bot_private_key
}
# Same pattern for eli-backend-api and eli-app
Secrets deployed to:
eli_hae_api: SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEYeli-backend-api: SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEYeli-app: SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY
Using in GitHub Actions Workflows
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.SDK_BOT_APP_ID }}
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
- name: Setup Node.js with GitHub Packages
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://npm.pkg.github.com'
scope: '@eli'
- name: Install dependencies
run: npm ci
env:
NODE_AUTH_TOKEN: ${{ steps.app-token.outputs.token }}
.npmrc Configuration
Each repo needs an .npmrc file (committed to repo):
@eli:registry=https://npm.pkg.github.com
Local Development
Developers need to authenticate locally to install SDK packages:
# One-time setup
npm login --registry=https://npm.pkg.github.com --scope=@eli
# When prompted:
# Username: your-github-username
# Password: <paste your personal access token with read:packages scope>
# Email: your-email@eli.health
Or add to ~/.npmrc:
//npm.pkg.github.com/:_authToken=ghp_your_personal_access_token
@eli:registry=https://npm.pkg.github.com
Retention Policy
| Package | Retention | Rationale |
|---|---|---|
| Latest 10 versions | Forever | Rollback capability |
| Older versions | 90 days | Storage cleanup |
SDK Update Notifications
This is not just any npm package - it's the API contract. When a new SDK version is published, developers need to know immediately.
Notification Mechanisms
1. Slack Notification (Immediate Awareness)
When an SDK is published, post to #eli-dev Slack channel with full changelog:
# In the SDK generation workflow
- name: Get previous version
id: prev-version
run: |
# Get the latest published version from GitHub Packages
PREV_VERSION=$(npm view @eli/hae-sdk version 2>/dev/null || echo "0.0.0")
echo "version=$PREV_VERSION" >> $GITHUB_OUTPUT
- name: Generate changelog
id: changelog
run: |
# Get commits since last SDK publish (compare with previous tag or hash)
if git rev-parse "sdk-hae-v${{ steps.prev-version.outputs.version }}" >/dev/null 2>&1; then
SINCE_TAG="sdk-hae-v${{ steps.prev-version.outputs.version }}"
else
# Fallback: get last 5 commits affecting API
SINCE_TAG="HEAD~5"
fi
# Generate formatted changelog
CHANGELOG=$(git log $SINCE_TAG..HEAD --pretty=format:"• %s" -- api/ app.py | head -10)
# Handle empty changelog
if [ -z "$CHANGELOG" ]; then
CHANGELOG="• Minor updates"
fi
# Export multiline for GitHub Actions
echo "changes<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Detect change type
id: change-type
run: |
# Use openapi-diff to detect if breaking change
if [ -f "previous-openapi.json" ]; then
DIFF_RESULT=$(npx openapi-diff previous-openapi.json openapi.json --format json 2>/dev/null || echo "{}")
BREAKING=$(echo "$DIFF_RESULT" | jq -r '.breakingDifferencesFound // false')
if [ "$BREAKING" = "true" ]; then
echo "type=:rotating_light: BREAKING CHANGE" >> $GITHUB_OUTPUT
echo "emoji=🚨" >> $GITHUB_OUTPUT
else
echo "type=:sparkles: New Version" >> $GITHUB_OUTPUT
echo "emoji=✨" >> $GITHUB_OUTPUT
fi
else
echo "type=:package: New Version" >> $GITHUB_OUTPUT
echo "emoji=📦" >> $GITHUB_OUTPUT
fi
- name: Notify Slack with changelog
if: success()
uses: slackapi/slack-github-action@v1
with:
channel-id: 'C0XXXXXXX' # #eli-dev
slack-message: |
${{ steps.change-type.outputs.type }}
*@eli/hae-sdk* `${{ steps.prev-version.outputs.version }}` → `${{ env.VERSION }}`
*What changed:*
${{ steps.changelog.outputs.changes }}
*Triggered by:* <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.event.head_commit.message }}>
*Author:* ${{ github.event.head_commit.author.name }}
_Downstream repos will receive auto-PRs._
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Tag this SDK release
run: |
git tag "sdk-hae-v${{ env.VERSION }}"
git push origin "sdk-hae-v${{ env.VERSION }}"
Example Slack Message:
✨ New Version
@eli/hae-sdk 1.2.3 → 1.2.4
What changed:
• Add support for batch image analysis
• Fix response type for cortisol readings
• Update validation schema for user metadata
Triggered by: feat: add batch analysis endpoint
Author: Zeeshan
Downstream repos will receive auto-PRs.
2. Automatic PR Creation (Actionable Update)
When SDK is published, automatically create a PR in consuming repos:
# In the SDK generation workflow - trigger downstream update
- name: Trigger Backend SDK Update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ steps.app-token.outputs.token }}
repository: eli-health/eli-backend-api
event-type: sdk-update
client-payload: |
{
"package": "@eli/hae-sdk",
"version": "${{ env.VERSION }}"
}
In the consuming repo (eli-backend-api):
# .github/workflows/sdk-update.yaml
name: Update SDK Dependency
on:
repository_dispatch:
types: [sdk-update]
jobs:
update-sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update package.json
run: |
npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
title: "Update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}"
body: |
## SDK Update
**Package:** `${{ github.event.client_payload.package }}`
**Version:** `${{ github.event.client_payload.previous_version }}` → `${{ github.event.client_payload.version }}`
## What Changed
${{ github.event.client_payload.changelog }}
## Change Type
${{ github.event.client_payload.is_breaking && '🚨 **BREAKING CHANGE** - Review carefully!' || '✅ Non-breaking change' }}
## Actions Required
- [ ] Review the changelog above
- [ ] Run `npm install` locally to test
- [ ] Verify TypeScript compilation passes
- [ ] Run tests: `npm test`
---
_Triggered by: [${{ github.event.client_payload.commit_message }}](${{ github.event.client_payload.commit_url }})_
branch: sdk-update/${{ github.event.client_payload.package }}
commit-message: "Update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}"
The upstream workflow sends this enhanced payload:
# In HAE SDK workflow - send changelog to downstream
- name: Trigger Backend SDK Update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ steps.app-token.outputs.token }}
repository: eli-health/eli-backend-api
event-type: sdk-update
client-payload: |
{
"package": "@eli/hae-sdk",
"version": "${{ env.VERSION }}",
"previous_version": "${{ steps.prev-version.outputs.version }}",
"changelog": "${{ steps.changelog.outputs.changes }}",
"is_breaking": ${{ steps.change-type.outputs.emoji == '🚨' }},
"commit_message": "${{ github.event.head_commit.message }}",
"commit_url": "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}"
}
3. Development Branch Auto-Update (Continuous Integration)
For development branches, auto-merge SDK updates to keep dev environments current:
# In the auto-PR workflow
- name: Enable Auto-Merge for Dev
if: github.base_ref == 'development'
run: gh pr merge --auto --squash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
Notification Flow
Breaking Change Alerts
For breaking changes (major version bump), add extra visibility with specific details about what broke:
- name: Detect breaking changes
id: breaking
if: env.VERSION_BUMP == 'major'
run: |
# Get detailed breaking changes from openapi-diff
BREAKING_DETAILS=$(npx openapi-diff previous-openapi.json openapi.json --format json | jq -r '
.breakingDifferences[]? |
"• \(.type): \(.path // .method) - \(.description // "Schema changed")"
' | head -5)
echo "details<<EOF" >> $GITHUB_OUTPUT
echo "$BREAKING_DETAILS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Alert Breaking Change
if: env.VERSION_BUMP == 'major'
uses: slackapi/slack-github-action@v1
with:
channel-id: 'C0XXXXXXX'
slack-message: |
:rotating_light: *BREAKING CHANGE* :rotating_light:
*@eli/hae-sdk* `${{ env.OLD_VERSION }}` → `${{ env.VERSION }}`
*Breaking changes detected:*
${{ steps.breaking.outputs.details }}
*What you need to do:*
1. Check auto-PR in downstream repos
2. Update code to handle the breaking changes
3. Run tests before merging
*Full diff:* <${{ github.server_url }}/${{ github.repository }}/compare/sdk-hae-v${{ env.OLD_VERSION }}...sdk-hae-v${{ env.VERSION }}|View on GitHub>
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Example Breaking Change Alert:
🚨 BREAKING CHANGE 🚨
@eli/hae-sdk 1.2.4 → 2.0.0
Breaking changes detected:
• removed: /api/v1/legacy-endpoint - Endpoint removed
• modified: POST /api/v1/analyze - Required field 'userId' added
• modified: Response schema - Field 'oldField' removed
What you need to do:
1. Check auto-PR in downstream repos
2. Update code to handle the breaking changes
3. Run tests before merging
Full diff: View on GitHub
Local Development
For developers working on both Backend and Mobile simultaneously on the same machine, there's a simple way to test SDK changes without waiting for CI/CD.
Setup
Assume you have both repos cloned as siblings:
~/projects/
├── eli-backend-api/
└── eli-app/
Script: Generate and Copy SDK Locally
Create this script in eli-backend-api/scripts/local-sdk.sh:
#!/bin/bash
set -e
echo "🔧 Generating Backend SDK for local development..."
# 1. Export OpenAPI spec from NestJS
echo "📄 Extracting OpenAPI spec..."
EXPORT_OPENAPI=true npm run build
# 2. Generate TypeScript SDK
echo "⚙️ Generating TypeScript SDK..."
mkdir -p sdk/src
npx @hey-api/openapi-ts -i openapi.json -o sdk/src -c @hey-api/client-fetch
# 3. Build the SDK
echo "🔨 Building SDK..."
cd sdk
cat > package.json << 'EOF'
{
"name": "@eli/backend-sdk",
"version": "0.0.0-local",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@hey-api/client-fetch": "^0.2.0"
}
}
EOF
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
EOF
npm install
npm run build
cd ..
# 4. Copy to eli-app if it exists as sibling
if [ -d "../eli-app" ]; then
echo "📦 Copying SDK to eli-app..."
rm -rf ../eli-app/node_modules/@eli/backend-sdk
mkdir -p ../eli-app/node_modules/@eli
cp -r sdk ../eli-app/node_modules/@eli/backend-sdk
echo "✅ SDK installed in eli-app/node_modules/@eli/backend-sdk"
else
echo "⚠️ eli-app not found as sibling directory"
echo " Copy sdk/ manually to your eli-app/node_modules/@eli/backend-sdk"
fi
echo ""
echo "🎉 Done! You can now test Backend API changes in the Mobile app."
Usage
# After making changes to Backend API endpoints
cd eli-backend-api
./scripts/local-sdk.sh
# eli-app immediately sees the new types
cd ../eli-app
npm run typecheck # Verify types match
Important Notes
- The local SDK version is
0.0.0-localto distinguish from published versions - Remember to run
npm installin eli-app after testing to restore the real SDK - This bypasses CI/CD - use only for local development, not for production code
- Always run proper tests before pushing changes
Migration Strategy
Adoption Steps
- Step 1: Set up SDK generation pipeline for HAE
- Step 2: Backend adopts
@eli/hae-sdk, validates with tests - Step 3: Set up SDK generation pipeline for Backend
- Step 4: Mobile adopts
@eli/backend-sdkfor critical services - Step 5: Migrate remaining mobile services incrementally
Parallel Operation Period
During migration, support both patterns:
// eli-app/src/services/user.ts
// Old pattern (remove after full migration)
import { IUser as IUserLegacy } from '@src/common/models';
import apiClient from '@shared/api.client';
// New pattern (SDK-based)
import { UsersService, IUser } from '@eli/backend-sdk';
// Feature flag for gradual rollout
export function getCurrentUser(): Promise<IUser> {
if (config.useSDK) {
return UsersService.getCurrentUser();
}
return apiClient.get<IUserLegacy>('/users/current') as Promise<IUser>;
}
Success Metrics
| Metric | Target | Measurement |
|---|---|---|
| Runtime type errors | 0 | Sentry error tracking |
| API contract violations caught at build time | 100% | CI failure logs |
| SDK generation time | < 2 min | GitHub Actions metrics |
| SDK package size | < 100KB | npm package stats |
| Developer adoption | 100% | Code review audits |
Risks and Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| OpenAPI spec incomplete | SDK missing types | Add Swagger decorators to all endpoints |
| Breaking changes too frequent | Build failures block deployment | Semantic versioning, deprecation warnings |
| SDK versioning conflicts | Dependency hell | Lock to exact versions in CI |
| Generated code quality | Developer friction | Review and customize generator templates |
Appendix
A. Infrastructure Configuration Files
Terraform (eli-devops repo):
github/variables.tf- SDK Bot variable definitionsgithub/secrets-sdk.tf- GitHub Actions secrets for SDK Botgithub/tfvars/github.tfvars- Actual secret values (gitignored)github/main.tf- Repository definitions
GitHub App:
- Management: https://github.com/organizations/eli-health/settings/apps/eli-sdk-bot
- App ID:
2659740 - Installed on: All eli-health repositories
B. Service File Locations
HAE API (eli_hae_api repo):
- OpenAPI endpoint:
GET /openapi.json - Main app:
app.py - Models:
api/models/ - Version file:
.version
Backend API (eli-backend-api repo):
- OpenAPI endpoint:
GET /docs-json - Main app:
src/main.ts - Swagger config: Lines 39-48 in
main.ts - Controllers:
src/modules/*/*.controller.ts - Models:
src/models/
Mobile App (eli-app repo):
- API Client:
src/shared/api.client.ts - Services:
src/services/ - Models:
src/common/models/
C. Deployment Pipeline
Builds are triggered by branch pushes:
| Branch | Environment | Project |
|---|---|---|
development | dev | eli-health-dev |
staging | staging | eli-health-staging |
main | production | eli-health-prod |
SDK generation integrates after the test phase, before Docker image build.
D. GitHub Secrets Deployed
Verify at: https://github.com/eli-health/{repo}/settings/secrets/actions
| Repository | Secret Name | Purpose |
|---|---|---|
| eli_hae_api | SDK_BOT_APP_ID | GitHub App ID for token generation |
| eli_hae_api | SDK_BOT_PRIVATE_KEY | Private key for token generation |
| eli-backend-api | SDK_BOT_APP_ID | GitHub App ID for token generation |
| eli-backend-api | SDK_BOT_PRIVATE_KEY | Private key for token generation |
| eli-app | SDK_BOT_APP_ID | GitHub App ID for token generation |
| eli-app | SDK_BOT_PRIVATE_KEY | Private key for token generation |