GeezerVison is showing github.com — Add Baseshift extension by whummer · Pull Request #173 · localstack/localstack-extensions
Conversation
whummer
commented
Sep 29, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
Summary
Adds an extension for Baseshift, which creates masked, writable clones of production databases for development. The extension runs the whole Baseshift flow locally next to LocalStack: replicating and masking a source database, publishing snapshot images to LocalStack ECR, and starting clones from them, so app code running in LocalStack can work against realistic but anonymized data.
- Self-hosted Dubs:
POST /dubs creates a Dub for a source database (e.g. LocalStack RDS) and runs the Baseshift agent and replication server as containers next to LocalStack (or optionally in a LocalStack EKS cluster). The extension drives the baseshift CLI, logged in as a system user (BASESHIFT_USER_EMAIL / BASESHIFT_USER_PASSWORD), applies an optional masking policy, and the masked snapshots end up in LocalStack ECR
- Clones API:
POST/GET/DELETE http://baseshift.localhost.localstack.cloud:4566/clones starts and stops clones on demand (e.g. one per PR), from the latest snapshot of a Dub ({"dub": "<name>"}) or from any clone image
- Default clone: set
BASESHIFT_IMAGE and a clone starts once LocalStack is ready, e.g. for a Dub that runs in your AWS account
- Connectivity: PostgreSQL clones are proxied through the gateway port 4566 (protocol handshake detection, like the ParadeDB extension) and published on host ports. MySQL clones only get a host port, since MySQL is server-speaks-first and can't be detected on the shared port
Architecture
flowchart TB
dev["Developer / CI<br/>psql, IDE, tests, ELT job"]
cloud["Baseshift Cloud (SaaS)<br/>control plane"]
subgraph docker["Docker host"]
subgraph ls["LocalStack container"]
gateway["Gateway :4566"]
subgraph ext["Baseshift extension"]
dubsapi["Dubs API<br/>(baseshift CLI)"]
clonesapi["Clones API"]
end
rds[("RDS PostgreSQL<br/>source data with PII")]
ecr[("ECR registry<br/>snapshot images")]
s3[("S3")]
snowflake[("Snowflake")]
end
subgraph dub["Dub containers"]
agent["Agent<br/>reads the source DB"]
repserver["Replication server<br/>masking, snapshots"]
end
subgraph clones["Clone containers"]
clone1[("Clone 'shop-dev'<br/>:5432")]
clone2[("Clone 'pr-123'<br/>:15432")]
end
end
cloud <-- "1 · create Dub, masking policy" --> dubsapi
dubsapi -- "2 · run" --> agent
rds -- "3 · logical replication" --> agent
agent -- "changes" --> repserver
cloud -. "manages" .-> repserver
repserver -- "4 · push masked snapshot" --> ecr
clonesapi -- "5 · start" --> clone1
ecr -. "pull" .-> clone1
gateway -- "SQL" --> clone1
dev -- "AWS APIs, SQL" --> gateway
dev -- "CSV export" --> s3
s3 -- "COPY INTO" --> snowflake
dev -. "SQL via host port" .-> clone2
Loading
- The Dubs API uses the
baseshift CLI to create the Dub in Baseshift Cloud and set its masking policy
- It starts the agent and replication server as containers (managed via Baseshift Cloud)
- The agent replicates the source DB via logical replication, the replication server masks it
- Masked snapshot images get pushed to LocalStack ECR
- The Clones API starts clone containers from them, reachable through the gateway or their host port
Demo
baseshift/demo/ has an end-to-end flow in one LocalStack container (Snowflake emulator image, which also serves RDS/ECR/S3):
RDS Postgres with PII → self-hosted Dub with masking policy → masked snapshot in LocalStack ECR → clone via the extension → export to S3 → COPY INTO Snowflake → the same record shown at each stage, with a check that no raw PII leaked.
make demo-selfhosted runs this with the real Baseshift components. make demo runs the same flow with a stand-in script for the masking step, which doesn't need a Baseshift user.
Output of a full local run:
column source (RDS) Baseshift clone Snowflake
-------- ---------------------------- ---------------------------- ----------------------------
name Jane Roe Darren Ortiz Darren Ortiz
email [email protected] [email protected] [email protected]
ssn 078-05-1120 MzeROCjpFL7 MzeROCjpFL7
phone +1 415 555 0142 iLC3dS3BsqZDBVK iLC3dS3BsqZDBVK
address 221B Baker Street, London 35493 North Trailville Stree 35493 North Trailville Stree
No raw PII values reached the clone or Snowflake.
Testing
- CI matrix: emulator
aws and snowflake × image tag latest and dev, using lstk, with postgres:17 as a stand-in clone image. All jobs run the integration tests (clones API, gateway and host port access, Dubs API validation), and the snowflake jobs also run make demo
- New
selfhosted-demo job runs make demo-selfhosted against the real Baseshift components, using the BASESHIFT_USER_EMAIL / BASESHIFT_USER_PASSWORD secrets. It takes about 8 minutes
- Verified locally with both deployments (Docker and EKS), including restarts and masking policy changes
Notes
- Each
selfhosted-demo run creates a new Dub (ci-<run id>) in the Baseshift org, since Dubs can't be deleted via the CLI. That's why the job is skipped for the nightly schedule and for fork PRs. It would be great to get a delete API from Baseshift
- Baseshift ignores the masking policy passed at creation for self-hosted Dubs, so the extension applies it after the initial snapshot and then deletes the unmasked image from ECR
- The replication server enrolls with a one-time token. With the Docker deployment, its certificates live in Docker volumes and survive LocalStack restarts. With EKS they don't, so a Dub needs a new name after a restart there
- LocalStack RDS doesn't apply the
rds.logical_replication parameter, so the demo sets wal_level=logical directly and reboots (the master user is a superuser in LocalStack)
- Running together with another extension that serves Postgres on port 4566 (e.g. ParadeDB) isn't supported, both claim the same handshakes
- Not yet added to CODEOWNERS
🤖 Generated with Claude Code
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add a LocalStack extension that runs Baseshift database clones (masked,
writable copies of production databases) next to LocalStack:
- default clone started once LocalStack is ready (BASESHIFT_IMAGE)
- clones API to start/stop clones on demand, e.g. from images in the
LocalStack ECR registry
- PostgreSQL clones proxied through the gateway port, plus host ports
- end-to-end demo: RDS source with PII, masked snapshot in ECR, clone,
and an ELT pipeline into the Snowflake emulator
- Helm values and docker compose setup for the Baseshift self-hosted
components against LocalStack (untested, requires image access)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
whummer
commented
Sep 29, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Commands for testing this end to end directly from the branch, no checkout needed. Requires Docker, lstk, psql, and LOCALSTACK_AUTH_TOKEN.
A plain postgres:17 image serves as a stand-in for a Baseshift clone image here (real clone images are private per Baseshift customer). lstk has no extension commands, so the extension is installed at startup via EXTENSION_AUTO_INSTALL, from the branch archive (git+https:// installs currently fail with the latest image due to the dulwich git shim).
# 1. Start LocalStack with the extension installed from the branch, and a default clone
LOCALSTACK_EXTENSION_AUTO_INSTALL="localstack-baseshift @ https://github.com/localstack/localstack-extensions/archive/refs/heads/add-baseshift-extension.tar.gz#subdirectory=baseshift" \
LOCALSTACK_BASESHIFT_IMAGE=postgres:17 \
LOCALSTACK_BASESHIFT_CLONE_POSTGRES_HOST_AUTH_METHOD=trust \
lstk start
# 2. Check the clone status (starts in the background once LocalStack is ready)
curl -s http://baseshift.localhost.localstack.cloud:4566/clones
# 3. Query the default clone, through the LocalStack gateway and on the host port
psql -h localhost.localstack.cloud -p 4566 -U postgres -c "SELECT version()"
psql -h localhost -p 5432 -U postgres -c "SELECT version()"
# 4. Push a "snapshot image" to LocalStack ECR, and start a second clone from it via the clones API
REPO=000000000000.dkr.ecr.us-east-1.localhost.localstack.cloud:4566/baseshift/my-dub
lstk aws ecr create-repository --repository-name baseshift/my-dub
docker tag postgres:17 "${REPO}:latest" && docker push "${REPO}:latest"
docker rmi "${REPO}:latest" # make sure the extension pulls it from the registry
curl -s -X POST http://baseshift.localhost.localstack.cloud:4566/clones \
-d "{\"name\": \"pr-123\", \"image\": \"${REPO}:latest\", \"env\": {\"POSTGRES_HOST_AUTH_METHOD\": \"trust\"}}"
curl -s http://baseshift.localhost.localstack.cloud:4566/clones/pr-123 # wait for "status": "running", "hostPort": 15432
psql -h localhost -p 15432 -U postgres -c "SELECT 'hello from pr-123'"
# 5. Stop the second clone, and clean up
curl -s -X DELETE http://baseshift.localhost.localstack.cloud:4566/clones/pr-123
lstk stop
Note: use "${REPO}:latest" with braces as above - in zsh, $REPO:latest is interpreted as the :l modifier.
Optional: the end-to-end demo (RDS source with PII → masked snapshot in ECR → clone → S3 → Snowflake). Requires a license that includes the Snowflake emulator, and runs in the Snowflake emulator image, which also provides RDS, ECR and S3:
LOCALSTACK_EXTENSION_AUTO_INSTALL="localstack-baseshift @ https://github.com/localstack/localstack-extensions/archive/refs/heads/add-baseshift-extension.tar.gz#subdirectory=baseshift" \
lstk start --type snowflake
mkdir baseshift-demo && cd baseshift-demo
curl -sO https://raw.githubusercontent.com/localstack/localstack-extensions/add-baseshift-extension/baseshift/demo/demo.py
curl -sO https://raw.githubusercontent.com/localstack/localstack-extensions/add-baseshift-extension/baseshift/demo/requirements.txt
python3 -m venv .venv && .venv/bin/pip install -q -r requirements.txt
.venv/bin/python demo.py all # or step by step: source, snapshot, clone, pipeline, compare
lstk stop
The last step prints the same customer record at every stage (raw in RDS, masked in the clone and in Snowflake), and checks that no raw PII leaked downstream.
Note: lstk start --type snowflake records snowflake as the emulator type in your lstk config. Use lstk start --type aws next time to switch back.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
- CI matrix over the aws and snowflake emulators; the snowflake jobs
also run the end-to-end demo
- demo: retry database work as a whole on connection errors, as RDS
instances may drop connections while (re)starting
- README: architecture diagram covering the full flow, incl. Baseshift
components and the Snowflake analytics pipeline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The extension can now create self-hosted Dubs via a new /dubs API, using the
baseshift CLI logged in as a system user (BASESHIFT_USER_EMAIL/PASSWORD). The
Baseshift components run as Docker containers next to LocalStack (default) or
in a LocalStack EKS cluster, push masked snapshots to LocalStack ECR, and
clones can be started from a Dub via POST /clones {"dub": ...}.
The demo uses this with --selfhosted, and a new CI job runs it end to end.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
amirdubhub
commented
Oct 22, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@whummer we recommend pinning the version of the baseshift dependency, eg
there's also an issue with the exposed port:
Clone ports are published on all interfaces
_run_clone publishes the clone's DB port without a host IP (ports.add(clone.port, DB_PORTS[clone.db_type])), so Docker binds it to 0.0.0.0 / [::]. Locally, docker port shows 0.0.0.0:15432, and I could connect through the host's non-loopback IP, not just localhost.
Combined with clones accepting connections without a password, anyone who can reach the host on that port (same LAN, shared CI runner, a dev VM with an open security group) can read the clone. Since Dubs are copies of production data, I'd like these bound to loopback by default:
ports = PortMappings(bind_host="127.0.0.1")
ports.add(clone.port, DB_PORTS[clone.db_type])
If there's a use case for remote access, maybe an opt-in config (e.g. BASESHIFT_CLONE_BIND_HOST) rather than the default. One thing to check: if anything reaches clones via host.docker.internal, loopback binding would break that path. Is there a reason not to go through the LocalStack Docker network instead?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|