Skip to content
2026-06-25

API Service

The API is a Laravel 12 application running on PHP 8.2+. It is the central backend: it manages collections, queries, tasks, and users; calls the NLP service for free-text extraction; runs async jobs via Redis queues; and handles authentication in both standalone and integrated modes.

  • Repo: cohort-discovery-service-api
  • Port: 8100 (dev)
  • Stack: PHP 8.2+, Laravel 12, MySQL 8.0, Redis, FrankenPHP (production)

Installation

composer install
npm install
cp .env.example .env
php artisan key:generate

Environment variables

Variable Default Description
APP_ENV local Environment: local, dev, or production
APP_DEBUG true Enable debug output — always false in production
APP_OPERATION_MODE standalone standalone or integrated — see Deployment Modes
APP_KEY Generated by php artisan key:generate
API_RATE_LIMIT 1000 Requests per minute per IP
TASK_TIMEOUT_SECONDS 300 Task execution timeout
LEASE_SECONDS 60 Collection host lease duration
COLLECTION_INACTIVITY_MINUTES 30 Threshold for inactive collection monitoring
Variable Default Description
DB_CONNECTION sqlite Use mysql for local dev and production
DB_HOST 127.0.0.1 MySQL host
DB_PORT 3306 MySQL port
DB_DATABASE App database name
DB_USERNAME MySQL user
DB_PASSWORD MySQL password
Variable Default Description
DB_OMOP_CONNECTION Leave blank to use primary DB; set to omop for a separate OMOP connection
DB_OMOP_HOST 127.0.0.1 OMOP vocab DB host
DB_OMOP_PORT 3306 OMOP vocab DB port
DB_OMOP_DATABASE omop OMOP vocab database name
DB_OMOP_USERNAME OMOP DB user
DB_OMOP_PASSWORD OMOP DB password
Variable Default Description
REDIS_CLIENT predis Redis driver
REDIS_HOST 127.0.0.1 Redis host
REDIS_PORT 6379 Redis port
REDIS_PASSWORD null Redis password
QUEUE_CONNECTION redis Queue backend
CACHE_STORE database Cache driver
Variable Default Description
JWT_SECRET JWT signing secret
STANDALONE_JWT_TTL_MINUTES 120 Token lifetime
PASSPORT_TOKEN_EXPIRES_IN_DAYS 30 Passport access token TTL
Variable Description
INTEGRATED_JWT_SECRET Must match the Gateway's JWT_SECRET
INTEGRATED_API_URI Gateway API base URL (e.g. http://localhost:8000/api/v1/)
INTEGRATED_AUTHORISATION_URI Gateway token endpoint (e.g. http://localhost:8000/oauth2/token)
INTEGRATED_CLIENT_ID OAuth client ID from Gateway seeder
INTEGRATED_CLIENT_SECRET OAuth client secret from Gateway seeder
OAUTH_INTERNAL_REDIRECT API callback URL (e.g. http://localhost:8100/auth/callback)
Variable Default Description
COHORT_DISCOVER_NLP_SERVICE_BASE_URI http://localhost:5001 NLP service URL
CLIENT_BASIC_AUTH_ENABLED true Enable basic auth for collection hosts
PENNANT_STORE database Feature flag storage (database or in-memory)
RULES_ENGINE_ACTIVE false Enable/disable experimental rules engine

OMOP database setup

The API has two database connections: the primary app DB and a separate OMOP vocabulary DB. Two setup paths are available:

No Athena account or large file downloads needed. Provides a minimal vocabulary sufficient for local development and testing.

mysql -u root -p -e "CREATE DATABASE omop;"

# Run OMOP schema migrations
php artisan migrate --database omop --path database/migrations_omop

# Seed with minimal OMOP concepts
php artisan db:seed --class=MinimalOmopSeeder --database=omop
  1. Register at Athena and export vocabularies (SNOMED, ICD-10, RxNorm, CO-CONNECT, etc.)
  2. Load into your OMOP database using omop-lite
  3. Configure DB_OMOP_* env vars to point at that database
  4. Optionally cache concept ancestors for faster hierarchy traversal:
    php artisan concepts:populate-ancestors
    

Running locally

composer run dev

This runs four processes concurrently via npx concurrently:

Process Command Purpose
Server php artisan serve Dev HTTP server on port 8100
Queue php artisan queue:listen --tries=1 Redis job queue listener
Logs php artisan pail --timeout=0 Real-time log streaming
Assets npm run dev Vite asset compilation

Database commands

# Fresh install with demo data (preferred for local dev)
php artisan migrate:fresh --seed

# Run pending migrations only
php artisan migrate

# Available seeders
php artisan db:seed --class=DatabaseSeeder             # Standard (default)
php artisan db:seed --class=DevDatabaseSeeder          # Production-like
php artisan db:seed --class=StandaloneDemoSeeder       # Demo users (standalone mode)
php artisan db:seed --class=MinimalOmopSeeder --database=omop
php artisan db:seed --class=MinimalDistributionsSeeder # Synthetic results (no BUNNY needed)
php artisan db:seed --class=CollectionSeeder
php artisan db:seed --class=RolesAndPermissionsSeeder

Custom artisan commands

# OMOP concepts
php artisan concepts:populate-ancestors     # Cache OMOP ancestor hierarchy for performance

# Task management
php artisan task:cleanup                    # Remove stale tasks
php artisan collection:no-activity-monitor  # Flag inactive collections
php artisan distributions:collector         # Collect distribution results

# Data import
php artisan import:omop-vocabs              # Bulk import OMOP vocabulary files
php artisan import:users-from-csv <file>    # Import users from CSV
php artisan admin:import-admin-users <file> # Import admin users

# Deployment
php artisan deploy:run                      # Run deployment steps (used by Docker start script)

# API documentation
php artisan l5-swagger:generate             # Regenerate Swagger / OpenAPI docs

# OAuth keys
php artisan passport:keys                   # Generate OAuth private/public keys
php artisan passport:client --personal      # Create personal access client

Testing

# Full test suite (clears config cache first — use for CI)
composer run test

# Fast iteration (skips cache clear)
composer run test:only

# Single test class or method
php artisan test --filter=TestClassName
php artisan test --filter=TestClassName::method_name

# With HTML coverage report
php artisan test --coverage

Tests run against a real MySQL database — daphne_test and omop_test. Configure these in .env.testing:

APP_ENV=testing
DB_CONNECTION=mysql
DB_DATABASE=daphne_test
DB_OMOP_CONNECTION=omop
DB_OMOP_DATABASE=omop_test

The base TestCase class provides JWT helpers:

$this->actingAsJwt($user);                  // Authenticate as a user in a test
$token = $this->makeJwtToken($user);         // Create a JWT token
$ids   = $this->idsFromOkResponse($response); // Extract ID array from 200 response
$this->disableMiddleware();                  // Skip middleware for isolated unit tests

Static analysis:

composer run phpstan

Linting (PSR-12):

composer run lint        # Check
composer run lint:fix    # Auto-fix

Workflow lint:

npm run lint:workflows   # Requires actionlint (brew install actionlint)


Project structure

app/
├── Console/Commands/         Artisan commands
├── Http/
│   ├── Controllers/Api/V1/  API endpoints — thin, delegate to Services
│   └── Middleware/           DecodeJwt, ClaimBasedAccessControl, CollectionHostBasicAuth, AuditRequests
├── Services/                 Business logic layer
│   ├── Authentication/       StandaloneAuthenticationService / IntegratedAuthenticationService
│   ├── Collections/          Collection host management
│   └── QueryContext/         Query execution backends (Beacon, BUNNY, FHIR)
├── Models/                   Eloquent models (Query, Collection, Task, User, …)
├── Policies/                 Laravel authorization policies
├── Jobs/                     Queued jobs (RunBeaconTask, ProcessDistribution, …)
├── Providers/                ApplicationModeServiceProvider — resolves auth mode at boot
└── Support/ApplicationMode.php   Mode-checking utility

database/
├── migrations/               Primary DB schema
├── migrations_omop/          OMOP vocabulary schema (runs on separate connection)
└── seeders/

routes/api.php                All /api/v1/ routes

config/
├── system.php                APP_OPERATION_MODE, feature flags
├── integrated.php            Integrated mode (JWT, OAuth URIs, client credentials)
└── database.php              Primary + OMOP DB connections

Checking the current mode in code:

use App\Support\ApplicationMode;

if (ApplicationMode::isStandalone()) { ... }
if (ApplicationMode::isIntegrated()) { ... }


Docker

docker build -t cohort-discovery-api:latest \
  --secret id=github_token,src=<(echo "$GITHUB_TOKEN") \
  .

The image uses FrankenPHP (Laravel Octane, PHP 8.4). Additional Docker env vars:

Variable Description
REBUILD_DB 1 to run migrate:fresh --seed; 0 to run migrate only
START_HORIZON 1 to start Laravel Horizon queue dashboard
PORT Container port (default 8080; typically remapped to 8100)