API version numbers reflect API contract changes (new/changed fields, new behaviour). The underlying framework is invisible to clients. Lift → http4s is a refactoring: it happens in-place inside the existing version file at the existing URL. No version bump.
Use a new version (e.g. v7.0.0) only when the API contract itself changes — new fields, changed request/response shape, new behaviour.
OBP-API runs as a single http4s Ember server (single process, single port). The application entry point is a Cats Effect IOApp (Http4sServer). Lift is no longer used as an HTTP server — Jetty and the servlet container have been removed.
Lift still plays two roles:
- ORM / Database — Lift Mapper manages schema creation, migrations, and data access.
- Legacy endpoint dispatch — Older API versions are handled through a bridge (
Http4sLiftWebBridge) that converts http4s requests into Lift requests, runs them through Lift's dispatch tables, and converts the responses back.
New API versions are implemented as native http4s routes and do not pass through the bridge.
Http4sServer extends IOApp. On startup it:
- Calls
bootstrap.liftweb.Boot().boot()to initialise Lift Mapper, connectors, and OBP configuration. - Parses the configured
hostnameanddev.portprops (defaults:127.0.0.1,8080). - Starts an Ember server with the application defined in
Http4sApp.httpApp.
Routes are tried in order: corsHandler (OPTIONS) → StatusPage → Http4s500 → Http4s700 → Http4sBGv2 → Http4s121 → Http4sLiftWebBridge (Lift fallback). Unhandled /obp/v7.0.0/* paths fall through silently to Lift — they do not 404.
HTTP Request
│
▼
Http4sServer (IOApp / Ember)
│
▼
corsHandler → StatusPage → Http4s500 → Http4s700 → Http4sBGv2 → Http4s121 → Http4sLiftWebBridge
│
LiftRules.statelessDispatch
LiftRules.dispatch (REST API)
│
▼
HTTP Response (with standard headers)
Handles any request not matched by a native http4s route:
- Reads the http4s request body.
- Constructs a Lift
Reqfrom the http4sRequest[IO]. - Creates a stateless Lift session.
- Initialises a Lift
Scontext and runsLiftRules.statelessDispatch/LiftRules.dispatch. - Handles Lift's
ContinuationExceptionpattern for async responses (timeout:http4s.continuation.timeout.ms, default 60 s). - Converts the Lift response back to http4s.
| Area | Role |
|---|---|
| Mapper ORM | Database schema creation, migrations, and all data access (MappedBank, AuthUser, etc.) |
| Boot | Initialises OBP configuration, connectors, resource docs, and Mapper schemifier |
| Dispatch tables | LiftRules.statelessDispatch / LiftRules.dispatch hold endpoint definitions for versions not yet ported |
| JSON utilities | Some serialisation helpers from net.liftweb.json are still in use |
| Before (Lift) | After (http4s) |
|---|---|
self: RestHelper => on the trait |
removed |
lazy val xyz: OBPEndpoint |
val xyz: HttpRoutes[IO] |
case "path" :: Nil JsonGet _ |
case req @ GET -> \prefixPath` / "path"` |
authenticatedAccess(cc) in for-comp |
pick the right EndpointHelpers.* helper |
implicit val ec = EndpointContext(Some(cc)) |
removed |
yield (json, HttpCode.\200`(cc))` |
yield json |
ResourceDoc(root, ...) |
ResourceDoc(null, ..., http4sPartialFunction = Some(root)) |
| Before | After |
|---|---|
extends OBPRestHelper |
removed |
registerRoutes(routes, allResourceDocs, apiPrefix) |
expose val allRoutes: HttpRoutes[IO] |
| registered via Boot / LiftRules | wired into Http4sServer chain |
See CLAUDE.md § Migrating a Lift Endpoint to http4s for the full Rule 1–5 reference.
Bottom-up — each version depends on the one below it being done.
Rule: one file = one PR. A file is either fully Lift or fully http4s — no half-converted state.
Note on APIMethods121: v1.2.1 was implemented as a new parallel file Http4s121.scala (rather than converting the Lift trait in-place) because APIMethods121 is a mixin trait inherited by APIMethods130, APIMethods140, etc. Converting the trait in-place would require all inheriting versions to be migrated simultaneously. The parallel file approach lets v1.2.1 go first — http4s routes take priority in the chain; the Lift trait remains until all inheriting versions are done, at which point the Lift trait can be deleted.
| # | File | Own endpoints | Notes |
|---|---|---|---|
| 1 | APIMethods121 |
70 | Done — Http4s121.scala serves all endpoints; 323 tests pass |
| 2 | APIMethods130 |
3 | Must follow #1 — OBPAPI1_3_0 mixes in all of APIMethods121 and registers those ~60 endpoints under the /obp/v1.3.0/ prefix. Migrating v1.3.0 before v1.2.1 would require porting all inherited endpoints anyway. |
| 3 | APIMethods140 |
11 | |
| 4 | APIMethods200 |
40 | |
| 5 | APIMethods210 |
28 | |
| 6 | APIMethods220 |
19 | |
| 7 | APIMethods300 |
47 | |
| 8 | APIMethods310 |
102 | |
| 9 | APIMethods400 |
~258 total | Largest file; may need splitting into sub-traits |
| 10 | APIMethods500 |
37 | |
| 11 | APIMethods510 |
111 | |
| 12 | APIMethods600 |
~244 total | Final Lift endpoint file |
Resource-docs endpoints are version-polymorphic: GET /obp/v6.0.0/resource-docs/v3.0.0/obp returns v3.0.0 docs. The URL prefix is cosmetically version-specific but functionally irrelevant — the API_VERSION path segment controls the output. This makes resource-docs a natural candidate for a single centralized http4s service rather than per-version handlers.
Add one service to Http4sApp (above the Lift bridge, before any per-version service) that handles:
GET /obp/*/resource-docs/API_VERSION/obp → version-dispatch via getResourceDocsList
GET /obp/*/resource-docs/API_VERSION/openapi.yaml
The wildcard prefix means all resource-doc requests are intercepted regardless of which version prefix the client uses. This workstream is independent of the per-version migration order — it can land at any time and immediately removes all resource-docs traffic from the Lift bridge.
V7ResourceDocsAggregationTest is intentionally failing. The current getResourceDocsObpV700 has a broken branch for requestedApiVersion == v7.0.0 that manually iterates allResourceDocs (~45 own docs) instead of calling getResourceDocsList, which aggregates all 500+. Fix this first — it is the same defect the centralized service must not repeat.
Currently served via a raw Lift serve { case Req(..., "openapi.yaml", ...) } block that bypasses registerRoutes entirely. Needs a dedicated http4s route (no ResourceDocMiddleware) added to the centralized service.
Caching.getStaticSwaggerDocCache() / setStaticSwaggerDocCache() are framework-agnostic and already used from within the http4s path. No migration work needed.
- Fix aggregation bug in
getResourceDocsObpV700→ makeV7ResourceDocsAggregationTestpass. - Extract shared handler logic into
Http4sResourceDocsservice; wire intoHttp4sApp. - Add
openapi.yamlroute to the same service. - Remove resource-docs from the per-version Lift objects (
ResourceDocs140–ResourceDocs600) once the centralized service covers them.
Token-generation paths — not version-file endpoints. Each extends RestHelper and needs to become an http4s route or middleware independently. Can run in parallel with the APIMethods migration.
| Component | Path | Notes |
|---|---|---|
DirectLogin |
POST /my/logins/direct |
|
GatewayLogin |
gateway JWT exchange | |
DAuth |
dAuth JWT exchange | |
OAuth |
OAuth 1.0a token endpoints | Most complex |
These are the last hard dependency on Lift Web in the request path. The Lift bridge cannot be removed until all four are done.
corsHandler
→ Http4sResourceDocs (/obp/*/resource-docs/*) ← centralized, all version prefixes
→ Http4s700 (/obp/v7.0.0/*)
→ Http4s600 (/obp/v6.0.0/*)
→ Http4s510 (/obp/v5.1.0/*)
→ Http4s500 (/obp/v5.0.0/*)
→ Http4s400 (/obp/v4.0.0/*)
→ Http4s310 (/obp/v3.1.0/*)
→ Http4s300 (/obp/v3.0.0/*)
→ Http4s220 (/obp/v2.2.0/*)
→ Http4s210 (/obp/v2.1.0/*)
→ Http4s200 (/obp/v2.0.0/*)
→ Http4s140 (/obp/v1.4.0/*)
→ Http4s130 (/obp/v1.3.0/*)
→ Http4s121 (/obp/v1.2.1/*)
→ Http4sBGv2
← Lift bridge removed
| Milestone | Condition |
|---|---|
| Version file done | All endpoints are HttpRoutes[IO]; OBPRestHelper removed from the file; existing tests pass |
| Lift bridge removable | All 12 APIMethods files done + auth stack done |
| Lift Web removed | lift-webkit removed from pom.xml; Boot.scala reduced to DB init + scheduler startup |
lift-mapper |
Separate long-term effort — not in scope here |
- Non-blocking I/O — Uses a small fixed thread pool (CPU cores) and suspends fibres on I/O. Thousands of concurrent requests without thread-pool tuning.
- Lower memory — No thread-per-request overhead.
- Modern Scala ecosystem — First-class Cats Effect, fs2 streaming, and functional patterns.
- No servlet container — Removes Jetty and WAR packaging entirely.
MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G" \
mvn -pl obp-api -am clean package -DskipTests=true -Dmaven.test.skip=true && \
java -jar obp-api/target/obp-api.jarBinds to hostname / dev.port from your props file (defaults: 127.0.0.1:8080).
| File | Status |
|---|---|
APIMethods121 |
done — Http4s121.scala (all 323 API1_2_1Test scenarios pass) |
APIMethods130 |
todo |
APIMethods140 |
todo |
APIMethods200 |
todo |
APIMethods210 |
todo |
APIMethods220 |
todo |
APIMethods300 |
todo |
APIMethods310 |
todo |
APIMethods400 |
todo |
APIMethods500 |
todo |
APIMethods510 |
todo |
APIMethods600 |
todo |
| Auth: DirectLogin | todo |
| Auth: GatewayLogin | todo |
| Auth: DAuth | todo |
| Auth: OAuth | todo |
| Resource-docs: aggregation bug fix | done |
Resource-docs: Http4sResourceDocs service |
todo |
Resource-docs: openapi.yaml route |
todo |
getCardsandgetCardsForBankremoved fromHttp4s700— these had the same API signature as the v1.3.0 originals and belonged inAPIMethods130, not v7.0.0. The Lift implementation inAPIMethods130serves them at/obp/v1.3.0/until that file is migrated.