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
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.
Running locally
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:
Linting (PSR-12):
Workflow lint:
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) |