A minimal Databricks App that:
- Connects to Lakebase (Databricks-managed Postgres) using a single
LAKEBASE_URLsecret (a native Postgres role with a static password) - Calls the Massive API (large paginated dataset) using a key stored in a Databricks secret scope
- Syncs Massive API data into Lakebase in batches
- Exposes a small Flask API to trigger syncs and read synced records
app.py- Flask app:/healthz,/records(GET),/sync(POST),/watchlist(GET/POST/DELETE),/news/sync(POST)lakebase.py- Lakebase connection helper (singleLAKEBASE_URL, psycopg2 + SQLAlchemy)massive_client.py- Massive API client: pagination generator for large datasets,get_latest_price,get_newssetup_secrets.py- One-time script to create the secret scopes and store the Massive API key + Lakebase URLapp.yaml- Databricks App deployment config (command + env vars)templates/index.html- Watchlist UI (add + remove tickers)notebooks/ingest_ticker_news_embeddings.py- Self-contained ETL notebook: reads tickers from thewatchlisttable, fetches news for those tickers directly from Massive (rate-limited to 5 requests/min for the free API tier) intoticker_news_documents, computes title/description embeddings intoticker_news_embeddings, and fetches + chunks + embeds each article's full body (viatrafilatura) intoticker_news_chunk_embeddings(pgvector)databricks.yml+resources/ingest_ticker_news_embeddings_job.yml- Databricks Asset Bundle config that schedules the notebook above as a Workflow (see Scheduling the embeddings notebook).env.example- Local dev env var template (copy to.env, do not commit real values)
- Go to https://massive.com and sign up for a new account (or log in if you already have one).
- Once logged in, open your account/workspace Settings (or Developer / API section, depending on Massive's current UI).
- Find API Keys and click Create API Key (or Generate New Key).
- Give the key a name (e.g.
databricks-app) and copy the generated key value immediately — most providers only show it once. - Keep this key handy for step 3 (Store your secrets) below. Do not put it in code,
.envcommitted to git, or anywhere else in plaintext.
If Massive's console differs from the steps above, look for API Keys, Tokens, or Credentials under your account/organization settings — the key is what authenticates requests to
https://api.massive.cominmassive_client.py.
-
In your Databricks workspace, go to Catalog (left sidebar) and select the Lakebase tab (or search "Lakebase" in the workspace search bar).
-
Click Create Lakebase instance (sometimes labeled Create database instance).
- Give it a name (e.g.
massive-sync-db). - Choose the capacity/compute size and region appropriate for your workload (defaults are fine to start).
- Click Create and wait for the instance to reach the Available/Running state.
- Give it a name (e.g.
-
Open the newly created instance, then go to the Roles & Databases tab (sometimes called Permissions or Roles).
-
Enable native (password) authentication for the instance if it isn't already on:
- Look for an authentication setting such as Native passwords or Password authentication and toggle/enable it. By default some Lakebase instances only support OAuth/token-based auth — you need password auth enabled so the role below gets a static password instead of a short-lived token.
-
Create a new role:
- Click Add role / Create role.
- Choose Password as the authentication method (not OAuth).
- Name the role (e.g.
massive_app) and let Databricks generate (or set) a password.
-
Copy the connection URL shown for the role. It will look like:
postgresql://<role>:<password>@<host>.database.cloud.databricks.com:5432/databricks_postgres?sslmode=requireKeep this URL — you'll paste it into
setup_secrets.py's prompt in the next step.
Run once from a Databricks notebook in your workspace (no CLI needed):
-
Create a new notebook (or open the Git folder you'll create in step 5, once it's cloned) and attach it to any running cluster.
-
In a cell, run:
%sh python setup_secrets.py
or open a terminal from the notebook (Run > Open terminal, if enabled on your cluster) and run
python setup_secrets.pythere.
This prompts (via getpass, so nothing is echoed or written to disk/shell history) for:
- Your Massive API key (from step 1) → stored as secret
massive/api-key - Your Lakebase connection URL (from step 2) → stored as secret
database/lakebase-url
Copy .env.example to .env and paste your Lakebase URL as LAKEBASE_URL for local runs:
cp .env.example .envFor deployment, app.yaml already pulls LAKEBASE_URL from the database/lakebase-url secret automatically — no manual editing needed there.
pip install -r requirements.txtpython app.pyAll of this is done through the Databricks workspace UI:
-
Create a Git folder:
- In the Databricks workspace sidebar, click Workspace > Create > Git folder (in older UIs this is called Repos > Add Repo).
- Paste the Git URL of this project's repository (e.g. your GitHub/GitLab remote for this codebase).
- Choose a folder name and click Create Git folder. Databricks will clone the repo directly into your workspace — this becomes the source for your app.
-
Create the Databricks App:
- In the sidebar, go to Compute > Apps (or search "Apps" in the workspace search bar).
- Click Create app, then choose Custom (or "From scratch").
- Give the app a name (e.g.
massive-lakebase-sync).
-
Point the app at your Git folder:
- When prompted for the source code location, select Workspace files / Git folder and browse to the Git folder you created in step 1 (the folder containing
app.pyandapp.yaml). - Databricks will read
app.yamlfrom that folder automatically to configure thecommandandenv(including theLAKEBASE_URL,MASSIVE_API_BASE_URL, and secret scope/key references).
- When prompted for the source code location, select Workspace files / Git folder and browse to the Git folder you created in step 1 (the folder containing
-
Deploy:
- Click Deploy (or Create and deploy) in the Apps UI. Databricks will build and start the app using the Git folder's current contents — no
databricksCLI commands are needed. - Whenever you update the code, pull the latest changes into the Git folder (Git folder > Pull, via the UI) and click Deploy again in the Apps UI to redeploy.
- Click Deploy (or Create and deploy) in the Apps UI. Databricks will build and start the app using the Git folder's current contents — no
-
Once deployed, open the app's URL from the Apps UI and hit
GET /healthzto confirm it's running, then tryPOST /syncto pull data from Massive into Lakebase.
GET /healthz- health checkGET /records?limit=100- read synced records from LakebasePOST /sync?batch_size=500with optional JSON body{"path": "/records"}- pull from Massive API and upsert into LakebaseGET /watchlist- get the current user's watchlist symbols with last known pricePOST /watchlist- add/update a symbol on the current user's watchlistDELETE /watchlist/<symbol>- remove a symbol from the current user's watchlistPOST /news/syncwith optional JSON body{"tickers": ["AAPL", "MSFT"], "limit": 50}- pull recent news per ticker from Massive and upsert intoticker_news_documents
notebooks/ingest_ticker_news_embeddings.py is a self-contained ETL: it reads the distinct
tickers from the watchlist table, fetches news for those tickers directly from Massive
(serially, rate-limited to max_requests_per_minute - 5/min by default, matching the free
Massive API tier's strict limits), and upserts them into ticker_news_documents. It then turns
those rows into vector embeddings in ticker_news_embeddings (title + description) and
ticker_news_chunk_embeddings (chunks of the full article body, fetched from each article's
article_url and extracted with trafilatura). You can run it on a schedule two ways — pick
whichever fits your setup:
This repo already includes bundle config for this: databricks.yml +
resources/ingest_ticker_news_embeddings_job.yml. This is the recommended path if you want the
job definition tracked in git alongside the code.
- Set the real workspace URL in
databricks.yml(replace<your-workspace-instance>). - Deploy:
databricks bundle deploy -t dev - Test it once manually:
databricks bundle run ingest_ticker_news_embeddings_job -t dev - Once you've confirmed a successful run, flip
pause_status: PAUSEDtopause_status: UNPAUSEDinresources/ingest_ticker_news_embeddings_job.ymland redeploy to turn on the daily schedule.
If you'd rather not use the CLI, you can create the equivalent job by hand in the Databricks UI:
- Get the notebook into your workspace: if you already created a Git folder for this repo
(see step 7 above), the notebook is already there at
notebooks/ingest_ticker_news_embeddings.py. Otherwise, upload/import it via Workspace > Create > Notebook > Import. - Create the job: go to Workflows (left sidebar) > Jobs > Create Job.
- Add a task:
- Task type: Notebook.
- Notebook path: browse to
notebooks/ingest_ticker_news_embeddings.pyin your Git folder. - Cluster: choose New job cluster (a small general-purpose cluster is enough) or an existing cluster/serverless, if available.
- Under Parameters, add the same widget values the notebook expects:
watchlist_table_name=watchlistnews_table_name=ticker_news_documentsembeddings_table_name=ticker_news_embeddingschunk_embeddings_table_name=ticker_news_chunk_embeddingsembedding_model=sentence-transformers/all-MiniLM-L6-v2massive_secret_scope=massivemassive_secret_key=api-keymassive_api_base_url=https://api.massive.comnews_fetch_limit=50max_requests_per_minute=5chunk_size=800chunk_overlap=100
- Add a schedule: click Add trigger on the job, choose Scheduled, and set it to run
daily (e.g. 6:00 AM UTC) using either the simple picker or a cron expression
(
0 0 6 * * ?, timezone UTC). - Add a failure notification: under Notifications, add your email/Slack webhook for on-failure alerts.
- Click Create and optionally Run now to validate the job before its first scheduled run.
Both options produce the same result — a Databricks Workflow that runs the notebook and refreshes
ticker_news_embeddings. The Asset Bundle keeps the definition in git and reproducible across
workspaces; the UI path is quicker for a one-off class demo but isn't tracked in version control.
Lakebase supports Change Data Feed (CDF), a managed way to stream row-level inserts/updates/deletes
from your Lakebase Postgres tables into Unity Catalog Delta tables (no Debezium, no custom connectors).
CDF is enabled per-schema in the databricks_postgres database, and every table in that schema that
meets two conditions is picked up automatically: it has REPLICA IDENTITY FULL set, and it has at least
one row.
Note: CDF is only available on paid Databricks accounts — it is not supported on the free Databricks Community Edition or trial tier.
By default, Postgres only logs primary-key columns on change. To capture full row contents (needed for
CDF), enable REPLICA IDENTITY FULL on each table — including watchlist and massive_records from
this app:
ALTER TABLE watchlist REPLICA IDENTITY FULL;
ALTER TABLE massive_records REPLICA IDENTITY FULL;Run this once per table, either from a Databricks SQL editor connected to your Lakebase instance, or
from a psql session using your LAKEBASE_URL. Any new table you add later (e.g. via ensure_table-style
helpers in app.py) needs the same ALTER TABLE ... REPLICA IDENTITY FULL statement run once before it
will be included in the feed. Tables with the setting but zero rows are skipped until the first row is
inserted, then picked up automatically.
You can confirm which tables currently qualify by querying:
SELECT * FROM wal2delta.tables;- In your Databricks workspace, open the Lakebase tab for your instance.
- Go to Lakebase CDF and click Start.
- Select the
databricks_postgresdatabase and the schema containing your tables (the default schema,public, works — it's insidedatabricks_postgres). - Choose the Unity Catalog destination schema/catalog where the CDF history tables should land.
- Confirm — the UI shows a preview of qualifying tables (e.g.
watchlist,massive_records) and their sync status before you start.
Once running, each qualifying table gets a corresponding Delta table named lb_<table_name>_history
(e.g. lb_watchlist_history) in Unity Catalog, updated roughly every 15 seconds. Each row includes
metadata columns (_pg_change_type, _pg_lsn, _pg_xid, _timestamp, _sort_by) describing the
change, so downstream Delta Live Tables/pipelines can build Silver/Gold layers off the append-only
history.
Note: Disabling CDF is lossy — changes made while it's off aren't captured, and re-enabling triggers a full resync (every row reloaded as an
insert). There's no per-table exclusion option within an enabled schema; the only way to keep a table out of the feed is to not setREPLICA IDENTITY FULLon it.
- Lakebase auth uses a single
LAKEBASE_URLsecret pointing at a native Postgres role with a static, non-expiring password — no token refresh logic needed inlakebase.py. - The Massive API pagination in
massive_client.pyassumes a{"items": [...], "next_cursor": ...}cursor-based shape. Adjustpaginated_getto match the real API's pagination contract. - For very large batch upserts, consider
psycopg2.extras.execute_valuesinstead of per-row inserts.