Post-Ejection Backend Feature List

Backend functionality to re-implement after ejecting from Base44 · Last updated 2026-07-03

39
Items

This page is the migration checklist for moving off Base44 onto the React + Postgres stack. Base44 currently owns authentication, the database/entity layer, file storage, and the Core AI integrations — all of which must be rebuilt. This list is kept current as new backend functionality is added to the app.

Authentication & Users

4

Base44 owns auth today (tokens, sessions, identity, the User entity). All of this must be rebuilt on the Postgres side.

User authentication backend
Large
To do

Login, signup, session/token issuance and verification. Currently handled entirely by the Base44 platform (base44.auth).

Replaces: base44.auth.me / login / logout / isAuthenticated
User entity & roles
Medium
To do

Users table with id, email, full_name, role (admin/user) plus custom profile fields (display_name, dark_mode, onboarding_complete).

Replaces: base44.entities.User
User invitations
Small
To do

Invite a user by email with an assigned role.

Replaces: base44.users.inviteUser
Admin impersonation (View As)
Medium
To do

Admin ability to view the platform as another user. Currently a client-side localStorage flag; needs a secure server-backed equivalent.

Replaces: Admin dashboard 'View As' flow

Database / Entities

5

Every Base44 entity becomes a Postgres table with equivalent CRUD, filtering, sorting and bulk operations.

Entity CRUD API
Large
To do

Create / read / update / delete, list with sort+limit, filter, bulkCreate, bulkUpdate, updateMany, deleteMany for all entities.

Replaces: base44.entities.*
Schema migration (DDL)
Large
To do

CREATE TABLE for all ~25 entities: AIModel, AIMarketplaceProduct, Dataset, Experiment, ExperimentRun, ClinicalObjective, InferenceRun, ModelTest, ValidationReport, ModelVersion, ModelDraft, Notification, PaymentMethod, MarketplaceListing, ListingReview, LeasedDataset, LeaseRequest, LeaseConfiguration, LeaseTransaction, DataConnection, ComplianceRule, DataQualityReport, DeanonymizationRequest, AuditLog, TestCase, AdminPermission, Revision, MosaicChat.

Replaces: Base44 entity schemas
Built-in record fields
Small
To do

id, created_date, updated_date, created_by_id auto-populated on every row.

Replaces: Base44 built-in attributes
Realtime subscriptions
Medium
To do

Live create/update/delete events used in the UI (e.g. entity.subscribe). Needs websockets / Postgres LISTEN-NOTIFY or polling.

Replaces: base44.entities.*.subscribe
Row-level access scoping
Medium
To do

User-scoped vs service-role data access (admins see all, users see their own).

Replaces: base44.asServiceRole vs user-scoped calls

Core Integrations (AI / Files / Email)

6

Base44 Core integrations used across the app. Each needs a direct provider integration after ejection.

LLM invocation
Large
To do

AI generation used for dataset analysis, quality reports, marketing copy, Mosaic Magic chat, field generation. Needs a direct OpenAI/Anthropic/Gemini integration with JSON-schema responses and optional web-search context.

Replaces: base44.integrations.Core.InvokeLLM
File upload (public)
Medium
To do

Uploads dataset files, model files, avatars; returns a public file_url.

Replaces: base44.integrations.Core.UploadFile
Private file upload + signed URLs
Medium
To do

Private storage with time-limited signed download URLs.

Replaces: Core.UploadPrivateFile / CreateFileSignedUrl
Extract data from uploaded file
Medium
To do

Parse CSV/XLSX/PDF/image into structured JSON against a schema (dataset import, schema extraction).

Replaces: Core.ExtractDataFromUploadedFile
Transactional email
Small
To do

App emails (contact form, notifications). Resend key already exists — wire it directly.

Replaces: Core.SendEmail + RESEND_API_KEY
Image generation
Small
To do

AI image generation where used in the UI.

Replaces: Core.GenerateImage

Backend Functions (custom logic)

9

Existing Deno backend functions. These are already standalone HTTP handlers and translate fairly directly into API routes.

runModelInference
Large
To do

Runs model inference (feature ordering + padding) — core ML path.

Replaces: functions/runModelInference
checkReplicatePrediction
Medium
To do

Polls Replicate prediction status (handles canceled + 10-min timeout).

Replaces: functions/checkReplicatePrediction
convertModelToTFJS
Medium
To do

Converts uploaded models to TensorFlow.js for browser inference.

Replaces: functions/convertModelToTFJS
extractModelFile / extractHDF5Metadata
Medium
To do

Parses uploaded model files and extracts HDF5 metadata.

Replaces: functions/extractModelFile, functions/extractHDF5Metadata
stripePayment / mockPurchaseModel
Medium
To do

Stripe payment processing and the marketplace purchase flow (ownership checks). Stripe keys already exist.

Replaces: functions/stripePayment, functions/mockPurchaseModel
sendNotification / sendEmail / sendContactEmail
Small
To do

Notification creation + email dispatch (incl. 2FA notifications, contact form).

Replaces: functions/sendNotification, sendEmail, sendContactEmail
getMyExperiments
Small
To do

Aggregated experiment retrieval for the current user.

Replaces: functions/getMyExperiments
testDbConnection
Medium
To do

Tests external data-source connections (Postgres, MySQL, Snowflake, etc.).

Replaces: functions/testDbConnection
adminFileExplorer / runBackendTests / testBackendPermissions / testReplicateIntegration
Medium
To do

Admin/dev tooling functions — lower priority for migration.

Replaces: functions/adminFileExplorer, runBackendTests, testBackendPermissions, testReplicateIntegration

Data Connections (external sources)

14

The Connections UI is now backend-agnostic: all calls route through src/components/connections/helpers/connectionService.js — the ONLY file to change at ejection. The new backend must implement the contract below. Today Base44 only does a TCP reachability check (no auth, no table count).

Canonical per-provider field mapping
Medium
Done

RESOLVED on current platform: the old 'cram every identifier into host' hack is gone. Each provider's real fields (Snowflake account_identifier, Databricks workspace_url, S3 bucket/region, Azure account_name/container, Mongo connection_uri) are collected and mapped explicitly by deriveConnectionMeta() for display metadata, while buildConnectionTarget() is the SINGLE source of truth for 'what endpoint (if any) do I actually connect to'. TESTABILITY classifies each type as 'tcp' (relational — real host:port probe) or 'driver_only' (object stores/warehouses/SRV — no valid TCP target). NEW BACKEND: keep deriveConnectionMeta for metadata but drive your real driver off buildConnectionTarget so a bucket name / account id is never treated as a hostname.

Replaces: connectionHelpers.buildConnectionTarget + TESTABILITY (single mapping seam)
Real driver-level connection test
Large
To do

Replace the TCP-only check with a genuine handshake per driver (Postgres, MySQL, MongoDB, Snowflake, BigQuery, S3, Azure Blob, Databricks): authenticate, verify the database/bucket exists, return table/collection count + latency. PLUG-AND-PLAY: the client already normalizes every result through interpretTestResult() into verified/reachable/failed and expects { success, authenticated, tables_available }. Today only 'tcp' types get a reachability probe; 'driver_only' types (and mongodb+srv, which resolves via SRV DNS to non-27017 hosts) return an honest 'reachable — pending backend verification' state (driver_verification_required:true) instead of a misleading DNS/port failure. The moment the backend returns authenticated:true it auto-promotes to 'verified' with NO client changes. All test/sync UI shares one interpreter, so the two-path inconsistency cannot recur.

Replaces: connectionService.testConnection → functions/testDbConnection
Encrypted credential storage
Large
To do

PLUG-AND-PLAY: the client splits secrets from metadata (splitCredentials), records which credential fields were provided (DataConnection.credential_fields — names only, never values), and hands secrets to connectionService.storeConnectionSecrets(). That function is the ONLY change point: today it returns { stored:false } (Base44 has no per-record vault) and the UI honestly shows 'provided but not yet vaulted'; swap its body for POST /connections/:id/secrets (envelope-encrypt via KMS/pgcrypto) + flip credentials_stored:true and the UI auto-shows 'securely stored'. Secrets are NEVER persisted client-side or returned to the browser.

Replaces: connectionService.storeConnectionSecrets → secure secret store
Schema browse (table/collection listing)
Medium
To do

PLUG-AND-PLAY: the UI (SchemaBrowserSheet, opened from each connection card) already renders the normalized shape from connectionService.browseSchema() — { available, reason?, tables:[{ name, rowCount?, columns:[{ name, type }] }] }. Today browseSchema() returns available:false with an honest 'pending backend' reason (no fabricated tables). New backend: implement POST /connections/:id/schema to introspect the live catalog via the vaulted driver and return the tables/columns array — swap the one line in browseSchema() and real tables render with ZERO UI change. tables_available on the card is likewise threaded through interpretTestResult.

Replaces: connectionService.browseSchema → POST /connections/:id/schema
SSRF protection + rate limiting
Medium
Done

RESOLVED on current backend: functions/testDbConnection now (1) rejects localhost/RFC1918/link-local/CGNAT literal hosts, (2) RESOLVES the hostname and rejects if ANY resolved A/AAAA record is private/internal (defeats DNS-rebinding to 169.254.169.254 / 10.x), returning 403 blocked:true, and (3) enforces an in-memory per-user rate limit (20/min) returning 429 rate_limited:true. Client also blocks early via isBlockedHost. NEW BACKEND: keep this exact allow/deny model at the network boundary and replace the in-memory limiter with a durable token bucket (Redis/Postgres) keyed by user+IP so it survives restarts and spans instances; ideally also disable HTTP redirects and pin the resolved IP for the actual connect.

Replaces: functions/testDbConnection SSRF guard + rate limiter
Connection CRUD + edit
Medium
To do

CRUD for DataConnection routes through connectionService (listConnections/createConnection/updateConnection/deleteConnection) — swap the four Base44 calls for REST. Full EDIT is now wired: the card's edit button opens ConnectionForm in edit mode (explodeConnectionMeta pre-fills stored metadata, type is locked, secrets are re-entered since they're never stored). The connection_type enum is reconciled across the entity, the form, the icon map, and the backend (postgresql, mysql, mongodb, snowflake, bigquery, s3, azure_blob, databricks) — no stale gcs/redshift references remain.

Replaces: connectionService CRUD (4 calls)
Atomic sync (no update race)
Small
Done

RESOLVED on current platform: sync no longer fires two separate update mutations (the old 'testing' write then a result write, which could interleave on rapid re-sync or unmount and persist a stale status). connectionService.syncConnection() now does test-then-single-write, and the page guards concurrent syncs per-connection via a syncingIds set. NEW BACKEND: expose this as one POST /connections/:id/sync that runs the driver test and updates the row in a single transaction.

Replaces: connectionService.syncConnection (single-write sync)
Background health check / auto-sync + drift
Medium
Done

RESOLVED (client scheduler): the Connections page periodically re-tests connections whose last check is stale (isHealthCheckDue, 5-min interval) via connectionService.healthCheckConnection(), which stamps DataConnection.last_checked (distinct from user-initiated last_sync) and reports DRIFT via detectDrift() (was 'active' → now 'error') as a warning toast. All health logic is pure in connectionHealth.js. NEW BACKEND: move the scheduler server-side — a cron worker calls POST /connections/:id/sync per due connection and returns the drift flag; the client effect then just reflects server state (or drops entirely).

Replaces: connectionService.healthCheckConnection + connectionHealth.js (client scheduler → server cron)
Duplicate detection
Small
Done

RESOLVED (client): on save, findDuplicateConnection() (connectionHealth.js) fingerprints type+host+database and warns if an identical connection already exists. Pure/deterministic. NEW BACKEND: enforce as a real uniqueness guard — a partial unique index on (owner, connection_type, host, database) or a 409 from POST /connections — so duplicates are blocked server-side, not just warned client-side.

Replaces: connectionHealth.findDuplicateConnection → DB unique constraint / 409
SSL/TLS mode selector
Small
Done

RESOLVED (client + schema): DataConnection.ssl_mode (disable/require/verify-ca/verify-full, default require) is collected in ConnectionForm for driver types (SSL_APPLIES_TO = postgres/mysql/mongodb; object stores/warehouses always use fixed HTTPS) and shown on the card and detail sheet. It's stored but NOT yet enforced (today's test is TCP-only). NEW BACKEND: pass ssl_mode into each driver's TLS config at connect time (libpq sslmode, mysql2 ssl, mongo tls) and fail closed on verify-ca/verify-full cert errors.

Replaces: DataConnection.ssl_mode → driver TLS config
Row preview (sample query)
Medium
To do

PLUG-AND-PLAY: QueryPreviewSheet renders the normalized shape from connectionService.queryPreview() — { available, reason?, columns:[], rows:[] }. Today available:false with an honest 'pending backend' reason (never fabricated rows). NEW BACKEND: POST /connections/:id/preview { table?, limit } runs a bounded, READ-ONLY SELECT ... LIMIT n through the vaulted driver and returns columns+rows — swap the one line in queryPreview(). Enforce read-only + a hard row cap server-side.

Replaces: connectionService.queryPreview → POST /connections/:id/preview
Connection usage / impact analysis
Medium
Done

RESOLVED (client seam) + schema: Dataset gained source_connection_id. connectionService.getConnectionUsage(id) queries datasets by it, and DeleteConnectionDialog shows dependent datasets BEFORE a delete so a connection can't be blindly removed. NEW BACKEND: back getConnectionUsage with GET /connections/:id/usage (SELECT id,name,record_count FROM datasets WHERE source_connection_id=:id) and ENSURE the import pipeline stamps source_connection_id on every dataset it creates from a connection.

Replaces: connectionService.getConnectionUsage → GET /connections/:id/usage
List UX — filter / search / sort / detail
Small
Done

RESOLVED (client, pure): connectionFilters.js (filterAndSortConnections + usedConnectionTypes) drives a ConnectionsToolbar (search by name/host/db, filter by type + status, sort by name/status/last-sync) — no backend needed today. Per-type empty guidance (ConnectionsEmptyState lists Databases/Warehouses/Object storage) and a distinct 'no results after filter' state (ConnectionsNoResults) replace the single generic empty prompt. The dormant ConnectionDetailSheet is now wired in (click a card name or the info button) for status/metadata/credentials/SSL/errors, with Sync/Edit/Preview delegated to the page so double-submit guarding and the atomic sync path stay in one place. NEW BACKEND: push filterAndSortConnections into GET /connections query params (search/type/status/sort) for server-side paging when lists grow large.

Replaces: connectionFilters.js + ConnectionsToolbar + ConnectionDetailSheet (client → GET /connections params)
Save/Sync double-submit + honest copy
Small
Done

RESOLVED (client UX): Save is disabled while the create/edit mutation is in flight (isPending threaded into ConnectionForm), Sync is guarded per-connection via a syncingIds set, and the form's test button shows a truthful 'Testing…' (a REAL backend reachability call) — the old fake 'Validating…' delay is gone. Credential copy is now migration-honest: 'never kept in your browser; used only at connection time and not persisted today; encrypted at rest once the backend is live.' No backend work — listed so the new team keeps the disabled-while-pending contract and swaps the copy the moment secrets are actually vaulted.

Replaces: ConnectionForm pending/disabled states + honesty copy (no backend change)

Secrets & Environment

1

Environment configuration currently managed by Base44 secrets.

API keys / secrets
Small
To do

RESEND_API_KEY, REPLICATE_API_KEY, STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY — move to the new app's environment/secret manager.

Replaces: Base44 secrets store

Pre-Ejection Code Review

CTO review before merging into the patient-data platform · Last updated 2026-08-03

5
Open blockers

PHI & Compliance (blockers)

4

This app was built against demo data. Several flows are unsafe the moment real patient data is present.

Dataset files live in PUBLIC storage
blocker

Uploads go through Core.UploadFile and Dataset.file_url is a public URL — anyone with the link can download the raw file. With patient data, ALL dataset/model uploads must move to private storage with short-lived signed URLs (UploadPrivateFile/CreateFileSignedUrl pattern, or the host platform's object store).

Raw data is sent to an external LLM
blocker

PARTIALLY FIXED: masking no longer touches the LLM — deidentifyDataset() is fully deterministic and local. Still open: runAnalytics() sends the first 6,000 characters of the uploaded file plus 15 sample rows to InvokeLLM for schema parsing and quality scoring. With PHI that remains an impermissible third-party disclosure without a BAA — switch analytics to the same deterministic parser (parseTabular) plus rule-based quality checks before merge.

'De-identified' status is cosmetic
blocker
Fixed

FIXED: de-identification now parses the entire file locally (deidentify/maskingEngine.js — deterministic CSV/JSON parse, no LLM), applies each field's strategy to every row, uploads the masked file and repoints Dataset.file_url at it, with a real row count. Remaining work for the host platform: (1) run this server-side on private storage instead of in the browser, (2) replace the dataset-id salt with a server-held HMAC key, (3) purge/lifecycle the pre-masking object, and (4) emit an immutable audit record of each de-id run.

Quality 'fix' does not modify data
blocker

applyQualityFix() asks the LLM to 'generate a summary of what was fixed' — no record is actually changed — then shows a success toast. This is a demo affordance; it must either be implemented for real or removed before clinicians see it.

Security & Access Control

5

Ownership is enforced in the browser, not the backend. The host platform must enforce all of this server-side (RLS / WHERE owner).

Unfiltered entity queries leak cross-user records
blocker

Open items from SECURITY_AUDIT.md are still live: DatasetDetail fetches DataQualityReport.list() and AIModel.list() with no owner filter; BuildModelDialog and RecentsGrid have the same pattern. Any user can receive other users' models/reports over the wire even if the UI hides them. Audit every .list() call and scope it, and add row-level security so it cannot regress.

Ownership checks are client-side only
blocker

DatasetDetail/ModelDetail throw 'Unauthorized' inside the React query function after the record is already fetched. Once the code talks to your API directly, these checks are trivially bypassed — enforce ownership in the API layer.

Admin impersonation is a localStorage flag
high

'View As' stores the target user in localStorage and the client filters accordingly. Any user can set that key. Needs a server-backed, audited impersonation session in the host platform.

Connection credentials & TLS not enforced
high

External DB credentials are intentionally NOT stored (connectionService.storeConnectionSecrets is a stub) and ssl_mode is collected but never enforced — the 'test' is a TCP reachability probe only. Already documented as plug-and-play seams in the feature list; the vault + real driver handshake are prerequisites for connecting anything to production data.

Rate limiting is in-memory
medium

testDbConnection's SSRF guard is solid, but its 20/min limiter resets on restart and doesn't span instances. Move to a durable token bucket (Redis/Postgres) keyed by user+IP.

Simulated / Demo Functionality

3

Features that look real but are mocked or partially wired. Each must be implemented for real, feature-flagged off, or clearly labeled before merge.

Marketplace purchases
high

mockPurchaseModel exists alongside stripePayment — confirm which path each purchase button uses and remove the mock path (or gate it to non-production) before the merged app can take payments.

Connection schema browse / row preview
medium

browseSchema() and queryPreview() honestly return 'pending backend' — good — but the backend endpoints (/schema, /preview with read-only + row caps) must exist before the Connections module is migrated.

Model metrics provenance
high

success_rate, ValidationReport contents and some analytics figures originate from LLM generation or seeds rather than measured runs. Before these numbers appear next to clinical data, tag each metric with its provenance (measured vs. estimated) or hide unverified ones.

Alerts & Notifications Integration

3

The host platform sets up clinical alerts — this app's notification layer must be reconciled with it, not merged blindly.

Deep links are app-relative
high

Notification.page_url stores routes like '/DatasetDetail?id=…'. Once embedded module-by-module into the host app these links break. Introduce a route-mapping layer (or store entity_type + entity_id and resolve the URL at render time).

Alert priority is heuristic
high

Priority is derived ad-hoc (e.g. quality score < 70 → 'high'). Align these thresholds with the host platform's clinical alerting rules so data-quality noise can't compete with patient-safety alerts.

Email paths & PHI in bodies
medium

sendEmail/sendContactEmail move to direct Resend after ejection. Review every email body template for PHI (dataset names, patient counts, error strings) and route through the host platform's compliant mailer.

Data Integrity & State Machines

3

Long-running statuses are driven by the browser; there is no server-side supervisor.

Statuses strand when the tab closes
high

Dataset can stick in 'de-identifying'/'analyzing' (one was manually repaired on 2026-08-01) and InferenceRun 'running' is only reconciled to 'cancelled' on page load. The host backend needs job supervision: server-side timeouts and a reconciliation sweep for any status older than its SLA.

No referential integrity
medium

Model↔dataset links are TWO arrays (AIModel.linked_dataset_ids and Dataset.linked_model_ids) that can drift; Dataset.source_connection_id is unenforced. In Postgres these become join tables / FKs with cascade rules — decide the canonical side before migrating either entity.

Derived counts trusted from estimates
medium

record_count, field_count and sensitive_fields_detected come from LLM parsing. Recompute deterministically at import time in the new pipeline.

Recommended Incremental Merge Order (Strangler)

6

Seams already exist for some modules; wrap the rest the same way before moving them. Feature-flag each migrated module and run contract tests against the live API docs.

1. Entities + private storage first
high

Stand up the ~28 tables and PRIVATE file storage in the host DB, read-only mirrored from Base44, so every later module lands on compliant storage from day one.

2. Auth/identity mapping
high

Map Base44 users (email, role, profile fields) to host-platform identities before any module that does ownership checks migrates. Kill the localStorage 'View As' at this step.

3. Datasets module (with real de-id pipeline)
high

Migrate dataset import/preview/quality with deterministic parsing and rule-based masking replacing the LLM calls — this is where patient data first flows, so it inherits items from the PHI section.

4. Connections (single-file swap)
medium

connectionService.js is the only file to change — implement the documented contract (driver handshake, vault, schema, preview, atomic sync) and swap it. The UI needs zero changes.

5. Models / inference, then marketplace last
medium

Inference (Replicate + ONNX) migrates once model files are on private storage. Marketplace/payments go last — they touch money and depend on everything else being stable.

Service-seam wrapping for remaining modules
high

Datasets, models, marketplace and notifications still call base44.* directly from pages/helpers. Before migrating each, extract its calls into a per-domain service module (the connectionService pattern) so cutover is a one-file swap with a feature flag.

UI Consistency & Cleanup

4

Smaller issues found during review.

'Incompatible' models toggle mislabeled
low
Fixed

The Dataset Detail models toggle showed 'Incompatible' for what is actually the partial-match bucket (<50% overlap but ≥1 matching column; zero-match models are excluded). Renamed to 'Partial' with corrected description.

Header title polling
low

The layout polls window.__pageTitle every 100ms to pick up page-title overrides. Replace with React context/state before merging into the host shell.

Page search list drifts from routes
low

The header search uses a hardcoded allPages array in Layout.jsx that isn't derived from the router — entries go stale as pages change. Derive it from the route config.

Timezone handling only fixed in one place
low

UTC-parse/local-display was fixed for inference runs; other timestamps (last_sync, notifications, audit log) should get the same treatment for consistency.

Effort estimates (Small / Medium / Large) are rough re-implementation sizing.