Migrate from Smartbox to Aidbox + FHIR App portal
Overview
This guide explains how to migrate from Smartbox to Aidbox + FHIR Portal by running both systems side by side on the same database. All examples in this guide demonstrate local installation using Docker. For Kubernetes deployment, follow the same steps but with Kubernetes-specific configurations.
Architecture (5 containers):
- 1 PostgreSQL database (
aidbox-db) - 2 Smartbox instances (existing):
portal- Smartbox portal on port 8888sandbox- Smartbox sandbox on port 9999
- 2 Aidbox instances (new):
aidbox-admin- Aidbox Admin on port 8080 (usesportaldatabase)aidbox-dev- Aidbox Dev on port 8090 (usessandboxdatabase)
Database mapping:
- Smartbox Portal + Aidbox Admin →
portaldatabase - Smartbox Sandbox + Aidbox Dev →
sandboxdatabase
Once all containers are running, you'll apply migrations to both Aidbox instances to transform the data structure. After migration, you can shut down the Smartbox containers.
Prerequisites
Required files in your project root:
- 1.
.env- Environment variables (see below) - 2.
docker-compose.yaml- 5-container setup (see below) - 3.
init-bundle.json- Init bundle for legacy FCE package - 4.
us-core-extensions.legacy.aidbox.tar.gz- Legacy FCE package file.
Migration Process
1. Reuse your existing .env file from Smartbox
Keep your existing Smartbox .env file with all the same values:
- Database credentials (
PGIMAGE,PGPORT,PGHOSTPORT,PGUSER,PGPASSWORD,PGDATABASE) - Licenses (
PORTAL_LICENSE,SANDBOX_LICENSE) - Smartbox image (
AIDBOX_IMAGE) - Client and admin credentials
The Aidbox containers will reuse these same environment variables to connect to the same database.
2. Create init-bundle.json
{
"type": "transaction",
"entry": [
{
"resource": {
"legacy-fce.aidbox#0.0.0": "file:///tmp/us-core-extensions.legacy.aidbox.tar.gz"
},
"request": {
"method": "POST",
"url": "/$upload-fhir-npm-packages"
}
}
]
}
3. Download us-core-extensions.legacy.aidbox.tar.gz
Place this file in the project root (same directory as docker-compose.yaml). Download this file here .
4. Create docker-compose.yaml (5-container setup)
version: '3.7'
services:
# Database service (shared by all containers)
aidbox-db:
image: "${PGIMAGE}"
ports:
- "${PGHOSTPORT}:${PGPORT}"
volumes:
- "./pgdata:/data"
environment:
POSTGRES_USER: "${PGUSER}"
POSTGRES_PASSWORD: "${PGPASSWORD}"
POSTGRES_DB: "${PGDATABASE}"
# Smartbox Portal (existing - port 8888)
portal:
image: "${AIDBOX_IMAGE}"
depends_on: ["aidbox-db"]
links:
- "aidbox-db:database"
- "sandbox:sandbox"
ports:
- "8888:8888"
env_file:
- .env
environment:
PGHOST: "database"
PGDATABASE: "portal"
BOX_AUTH_LOGIN__REDIRECT: "/admin/portal"
BOX_PROJECT_ENTRYPOINT: "smartbox.portal/box"
AIDBOX_LICENSE: "${PORTAL_LICENSE}"
AIDBOX_BASE_URL: "http://localhost:8888"
BOX_SMARTBOX_SANDBOX__URL: "http://sandbox:8888"
BOX_SMARTBOX_SANDBOX__BASIC: "${AIDBOX_CLIENT_ID}:${AIDBOX_CLIENT_SECRET}"
# Smartbox Sandbox (existing - port 9999)
sandbox:
image: "${AIDBOX_IMAGE}"
depends_on: ["aidbox-db"]
links:
- "aidbox-db:database"
ports:
- "9999:8888"
env_file:
- .env
environment:
PGHOST: "database"
PGDATABASE: "sandbox"
BOX_AUTH_LOGIN__REDIRECT: "/"
BOX_PROJECT_ENTRYPOINT: "smartbox.dev-portal/box"
AIDBOX_LICENSE: "${SANDBOX_LICENSE}"
AIDBOX_BASE_URL: "http://localhost:9999"
# Aidbox Dev (new - port 8090, uses sandbox database)
aidbox-dev:
image: healthsamurai/aidboxone:edge
pull_policy: always
depends_on:
- aidbox-db
links:
- "aidbox-db:database"
ports:
- 8090:8090
volumes:
- ./init-bundle.json:/tmp/initBundle.json:ro
- ./us-core-extensions.legacy.aidbox.tar.gz:/tmp/us-core-extensions.legacy.aidbox.tar.gz:ro
environment:
BOX_ADMIN_PASSWORD: password
BOX_BOOTSTRAP_FHIR_PACKAGES: hl7.fhir.r4.core#4.0.1:hl7.fhir.us.core#6.1.0
BOX_COMPATIBILITY_VALIDATION_JSON__SCHEMA_REGEX: "#{:fhir-datetime}"
BOX_DB_DATABASE: sandbox # Same as Smartbox sandbox
BOX_DB_HOST: database
BOX_DB_PASSWORD: "${PGPASSWORD}"
BOX_DB_PORT: "${PGPORT}"
BOX_DB_USER: "${PGUSER}"
BOX_FHIR_COMPLIANT_MODE: "true"
BOX_FHIR_CORRECT_AIDBOX_FORMAT: "true"
BOX_FHIR_CREATEDAT_URL: https://aidbox.app/ex/createdAt
BOX_FHIR_SCHEMA_VALIDATION: "true"
BOX_FHIR_SEARCH_AUTHORIZE_INLINE_REQUESTS: "true"
BOX_FHIR_SEARCH_CHAIN_SUBSELECT: "true"
BOX_FHIR_SEARCH_COMPARISONS: "true"
BOX_FHIR_TERMINOLOGY_ENGINE: hybrid
BOX_FHIR_TERMINOLOGY_ENGINE_HYBRID_EXTERNAL_TX_SERVER: https://tx.health-samurai.io/fhir
BOX_FHIR_TERMINOLOGY_SERVICE_BASE_URL: https://tx.health-samurai.io/fhir
BOX_LICENSE: "${SANDBOX_LICENSE}" # Reuse sandbox license
BOX_MODULE_SDC_STRICT_ACCESS_CONTROL: "true"
BOX_ROOT_CLIENT_SECRET: l1sE4McoQw
BOX_SEARCH_INCLUDE_CONFORMANT: "true"
BOX_SECURITY_AUDIT_LOG_ENABLED: "true"
BOX_SECURITY_DEV_MODE: "true"
BOX_SETTINGS_MODE: read-write
BOX_WEB_BASE_URL: http://dev.localhost:8090
BOX_WEB_PORT: 8090
BOX_INIT_BUNDLE: "file:///tmp/initBundle.json"
BOX_FHIR_LEGACY_FCE_PACKAGE: us-core-extensions.legacy.aidbox#0.0.0
healthcheck:
test: curl -f http://dev.localhost:8090/health || exit 1
interval: 5s
timeout: 5s
retries: 90
start_period: 30s
# Aidbox Admin (new - port 8080, uses portal database)
aidbox-admin:
image: healthsamurai/aidboxone:edge
pull_policy: always
depends_on:
- aidbox-db
links:
- "aidbox-db:database"
ports:
- 8080:8080
volumes:
- ./init-bundle.json:/tmp/initBundle.json:ro
- ./us-core-extensions.legacy.aidbox.tar.gz:/tmp/us-core-extensions.legacy.aidbox.tar.gz:ro
environment:
BOX_ADMIN_PASSWORD: password
BOX_BOOTSTRAP_FHIR_PACKAGES: hl7.fhir.r4.core#4.0.1:hl7.fhir.us.core#6.1.0
BOX_COMPATIBILITY_VALIDATION_JSON__SCHEMA_REGEX: "#{:fhir-datetime}"
BOX_DB_DATABASE: portal # Same as Smartbox portal
BOX_DB_HOST: database
BOX_DB_PASSWORD: "${PGPASSWORD}"
BOX_DB_PORT: "${PGPORT}"
BOX_DB_USER: "${PGUSER}"
BOX_FHIR_COMPLIANT_MODE: "true"
BOX_FHIR_CORRECT_AIDBOX_FORMAT: "true"
BOX_FHIR_CREATEDAT_URL: https://aidbox.app/ex/createdAt
BOX_FHIR_SCHEMA_VALIDATION: "true"
BOX_FHIR_SEARCH_AUTHORIZE_INLINE_REQUESTS: "true"
BOX_FHIR_SEARCH_CHAIN_SUBSELECT: "true"
BOX_FHIR_SEARCH_COMPARISONS: "true"
BOX_FHIR_TERMINOLOGY_ENGINE: hybrid
BOX_FHIR_TERMINOLOGY_ENGINE_HYBRID_EXTERNAL_TX_SERVER: https://tx.health-samurai.io/fhir
BOX_FHIR_TERMINOLOGY_SERVICE_BASE_URL: https://tx.health-samurai.io/fhir
BOX_LICENSE: "${PORTAL_LICENSE}" # Reuse portal license
BOX_MODULE_SDC_STRICT_ACCESS_CONTROL: "true"
BOX_ROOT_CLIENT_SECRET: l1sE4McoQw
BOX_SEARCH_INCLUDE_CONFORMANT: "true"
BOX_SECURITY_AUDIT_LOG_ENABLED: "true"
BOX_SECURITY_DEV_MODE: "true"
BOX_SECURITY_ORGBAC_ENABLED: "true"
BOX_SETTINGS_MODE: read-write
BOX_WEB_BASE_URL: http://admin.localhost:8080
BOX_WEB_PORT: 8080
BOX_INIT_BUNDLE: "file:///tmp/initBundle.json"
BOX_SECURITY_AUTH_KEYS_SECRET: "very-secret"
BOX_FHIR_LEGACY_FCE_PACKAGE: us-core-extensions.legacy.aidbox#0.0.0
healthcheck:
test: curl -f http://admin.localhost:8080/health || exit 1
interval: 5s
timeout: 5s
retries: 90
start_period: 30s
5. Start all containers
docker-compose up -d
This will start:
- aidbox-db on port 5437
- portal (Smartbox) on port 8888
- sandbox (Smartbox) on port 9999
- aidbox-admin on port 8080
- aidbox-dev on port 8090
6. Apply Migrations
Once all 5 containers are running and healthy, apply the migrations using the /db/migrations endpoint.
Important: Admin and Dev Aidbox instances require different migrations:
- Admin Aidbox (portal): Apply all 3 migrations (has Tenant resources)
- Dev Aidbox (sandbox): Apply only Client migration (no Tenant resources)
6a. Apply Migrations to Admin Aidbox (port 8080)
POST /db/migrations
content-type: text/yaml
accept: text/yaml
- id: sb-001-tenant-to-organization
sql: |
INSERT INTO organization (
id, txid, ts, cts, resource_type, status, resource
)
SELECT
t.id,
t.txid,
t.ts,
t.cts,
'Organization',
t.status,
jsonb_build_object(
'resourceType', 'Organization',
'id', t.id,
'name', COALESCE(t.resource->>'name', t.id),
'active', true,
'meta', jsonb_build_object(
'lastUpdated', t.ts,
'versionId', t.txid::text
)
)
|| COALESCE(
(
SELECT jsonb_object_agg(key, value)
FROM jsonb_each(t.resource)
WHERE key NOT IN ('name','resourceType','id','extension')
),
'{}'::jsonb
)
FROM tenant t
WHERE t.status = 'created'
ON CONFLICT (id) DO UPDATE
SET resource = EXCLUDED.resource,
ts = EXCLUDED.ts,
status = EXCLUDED.status;
- id: sb-002-tenant-meta-to-extension
sql: |
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT table_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND column_name = 'resource'
AND data_type = 'jsonb'
LOOP
EXECUTE format($f$
UPDATE %I
SET resource =
jsonb_set(
(CASE
WHEN resource->'meta' IS NULL
THEN jsonb_set(resource, '{meta}', '{}'::jsonb)
ELSE resource
END #- '{meta,tenant}'),
COALESCE(resource->'meta'->'extension','[]'::jsonb)
|| jsonb_build_array(
jsonb_build_object(
'url','https://aidbox.app/tenant-organization-id',
'value', jsonb_build_object(
'Reference', jsonb_build_object(
'id', resource->'meta'->'tenant'->'id',
'resourceType','Organization'
)
)
)
)
)
WHERE resource->'meta'->'tenant' IS NOT NULL
AND resource->'meta'->'tenant' <> 'null'::jsonb
$f$, rec.table_name);
END LOOP;
END;
$$;
- id: sb-003-client-resource-migration
sql: |
UPDATE client
SET resource = (
SELECT CASE
WHEN resource->>'type' IN ('provider-facing-smart-app','patient-facing-smart-app')
THEN jsonb_set(resource, '{type}', '"smart-app"'::jsonb)
ELSE resource
END
)
WHERE resource->>'type' IN ('provider-facing-smart-app','patient-facing-smart-app');
UPDATE client
SET resource =
CASE
WHEN resource->'details'->'user-id' IS NOT NULL
OR resource->'details'->'status' IS NOT NULL
THEN
jsonb_set(
CASE
WHEN resource->'meta' IS NULL THEN jsonb_set(resource, '{meta}', '{}'::jsonb)
ELSE resource
END #- '{details,user-id}' #- '{details,status}',
'{meta,extension}',
COALESCE(resource->'meta'->'extension','[]'::jsonb)
|| (CASE WHEN resource->'details'->'user-id' IS NOT NULL
THEN jsonb_build_array(jsonb_build_object(
'url','http://aidbox.app/StructureDefinition/Client/created-by',
'value', jsonb_build_object(
'Reference', jsonb_build_object(
'id', resource->'details'->'user-id',
'resourceType', 'User'
)
)
))
ELSE '[]'::jsonb END)
|| (CASE WHEN resource->'details'->'status' IS NOT NULL
THEN jsonb_build_array(jsonb_build_object(
'url','http://aidbox.app/StructureDefinition/Client/status',
'value', jsonb_build_object(
'Code',
CASE resource->'details'->>'status'
WHEN 'in-review' THEN 'review'
WHEN 'not-synced' THEN 'draft'
WHEN 'synced' THEN 'active'
ELSE resource->'details'->>'status'
END
)
))
ELSE '[]'::jsonb END)
)
ELSE resource
END
WHERE resource->'details'->'user-id' IS NOT NULL
OR resource->'details'->'status' IS NOT NULL;
UPDATE client
SET resource = resource #- '{details}'
WHERE resource->'details' = '{}'::jsonb;
6b. Apply Migrations to Dev Aidbox (port 8090)
Dev Aidbox (sandbox) does not have Tenant resources, so we only apply the Client migration:
POST /db/migrations
content-type: text/yaml
accept: text/yaml
- id: sb-003-client-resource-migration
sql: |
UPDATE client
SET resource = (
SELECT CASE
WHEN resource->>'type' IN ('provider-facing-smart-app','patient-facing-smart-app')
THEN jsonb_set(resource, '{type}', '"smart-app"'::jsonb)
ELSE resource
END
)
WHERE resource->>'type' IN ('provider-facing-smart-app','patient-facing-smart-app');
UPDATE client
SET resource =
CASE
WHEN resource->'details'->'user-id' IS NOT NULL
OR resource->'details'->'status' IS NOT NULL
THEN
jsonb_set(
CASE
WHEN resource->'meta' IS NULL THEN jsonb_set(resource, '{meta}', '{}'::jsonb)
ELSE resource
END #- '{details,user-id}' #- '{details,status}',
'{meta,extension}',
COALESCE(resource->'meta'->'extension','[]'::jsonb)
|| (CASE WHEN resource->'details'->'user-id' IS NOT NULL
THEN jsonb_build_array(jsonb_build_object(
'url','http://aidbox.app/StructureDefinition/Client/created-by',
'value', jsonb_build_object(
'Reference', jsonb_build_object(
'id', resource->'details'->'user-id',
'resourceType', 'User'
)
)
))
ELSE '[]'::jsonb END)
|| (CASE WHEN resource->'details'->'status' IS NOT NULL
THEN jsonb_build_array(jsonb_build_object(
'url','http://aidbox.app/StructureDefinition/Client/status',
'value', jsonb_build_object(
'Code',
CASE resource->'details'->>'status'
WHEN 'in-review' THEN 'review'
WHEN 'not-synced' THEN 'draft'
WHEN 'synced' THEN 'active'
ELSE resource->'details'->>'status'
END
)
))
ELSE '[]'::jsonb END)
)
ELSE resource
END
WHERE resource->'details'->'user-id' IS NOT NULL
OR resource->'details'->'status' IS NOT NULL;
UPDATE client
SET resource = resource #- '{details}'
WHERE resource->'details' = '{}'::jsonb;
7. Post-Migration Verification
the Now you can run Inferno G10 test kit on both Aidbox instances to verify compliance with G10 certification. See Pass Inferno Tests with Aidbox for step-by-step instructions.
API Changes: Smartbox → Aidbox
If you have applications built on top of Smartbox API, you need to update your API calls to use the new Aidbox endpoint patterns.
Endpoint Pattern Comparison
| API Category | Smartbox Pattern | Aidbox Pattern | Notes |
|---|---|---|---|
| US Core FHIR Resources (Patient-facing) | /tenant/{tenant}/patient/smart-api/{ResourceType} | /Organization/{org-id}/fhir/{ResourceType} | Tenant replaced with Organization |
| US Core FHIR Resources (Provider-facing) | /tenant/{tenant}/provider/smart-api/{ResourceType} | /Organization/{org-id}/fhir/{ResourceType} | Same endpoint for both patient & provider |
| FHIR Resource by ID | /tenant/{tenant}/patient/smart-api/{ResourceType}/{id} | /Organization/{org-id}/fhir/{ResourceType}/{id} | Resource ID access |
| FHIR Search | GET/POST /tenant/{tenant}/patient/smart-api/{ResourceType}?param=value | GET/POST /Organization/{org-id}/fhir/{ResourceType}?param=value | Search parameters unchanged |
| OAuth2 - Authorize | /tenant/{tenant}/patient/auth/authorize | /auth/authorize | OAuth2 authorization |
| OAuth2 - Token | /tenant/{tenant}/patient/auth/token | /auth/token | Token endpoint |
| Provider Auth - Authorize | /tenant/{tenant}/provider/auth/authorize | /auth/authorize | No separate provider auth |
| SMART Configuration | /tenant/{tenant}/patient/smart-api/.well-known/smart-configuration | /.well-known/smart-configuration | SMART metadata endpoint |
| FHIR Metadata | /tenant/{tenant}/patient/smart-api/metadata | /Organization/{org-id}/fhir/metadata | CapabilityStatement |
| Bulk Data - Patient Export | /fhir/Patient/$export | /fhir/Patient/$export | ✅ No change |
| Bulk Data - Group Export | /fhir/Group/{id}/$export | /fhir/Group/{id}/$export | ✅ No change |
| Bulk Data - Status | /fhir/bulkstatus/{request-id} | /fhir/bulkstatus/{request-id} | ✅ No change |
| C-CDA Document Generation | /ccda/make-doc | /ccda/make-doc | ✅ No change |
Key Changes Summary
- 1.Tenant → Organization (FHIR Resources only): The primary change is replacing
/tenant/{tenant-id}with/Organization/{org-id}for FHIR resource endpoints - 2.Unified Patient/Provider: Aidbox uses a single set of endpoints instead of separate
/patient/and/provider/paths - 3.Removed
/smart-api/path segment: FHIR resources are now directly under/fhir/instead of/smart-api/ - 4.Auth Consolidation: Authentication endpoints are now at root level (
/auth/*) without tenant prefix - 5.Bulk Data & C-CDA: These endpoints remain at root level and do not change between Smartbox and Aidbox