Skip to content

Commit 3bdcea4

Browse files
fazzoneabc3acco
authored
🪞🐘 Support for Postgres Replicas (#1414)
Co-authored-by: abc3 <sts@abc3.dev> Co-authored-by: Anthony Accomazzo <accomazz@gmail.com>
1 parent a8c62d6 commit 3bdcea4

File tree

23 files changed

+1122
-172
lines changed

23 files changed

+1122
-172
lines changed

assets/svelte/databases/Form.svelte

Lines changed: 151 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@
5858
publication_name: string;
5959
slot_name: string;
6060
useLocalTunnel: boolean;
61+
is_replica: boolean;
62+
primary: {
63+
ssl: boolean;
64+
database: string;
65+
hostname: string;
66+
port: number;
67+
username: string;
68+
password: string;
69+
};
6170
};
6271
export let errors: Record<string, any> = {};
6372
export let submitError: string | null = null;
@@ -69,7 +78,7 @@
6978
export let showPgVersionWarning: boolean = false;
7079
export let selfHosted: boolean = false;
7180
72-
let form = { ...database, ssl: true }; // Set default SSL to true
81+
let form = { ...database };
7382
7483
const isEdit = !!form.id;
7584
@@ -490,6 +499,133 @@ sequin tunnel --ports=[your-local-port]:${form.name}`;
490499
{#if databaseErrors.ssl}
491500
<p class="text-destructive text-sm">{databaseErrors.ssl}</p>
492501
{/if}
502+
503+
<div class="flex items-center gap-2">
504+
<Switch id="is_replica" bind:checked={form.is_replica} />
505+
<Label for="is_replica" class="flex items-center">
506+
Replica
507+
<Tooltip.Root openDelay={200}>
508+
<Tooltip.Trigger>
509+
<HelpCircle class="inline-block h-4 w-4 text-gray-400 ml-1" />
510+
</Tooltip.Trigger>
511+
<Tooltip.Content class="max-w-xs">
512+
<p class="text-sm text-gray-500">
513+
<b>Replica</b>
514+
<br />
515+
When connecting a replica to Sequin, Sequin also needs to connect
516+
to the primary database.
517+
<a
518+
href="https://docs.sequinstream.com/reference/databases#using-sequin-with-a-replica"
519+
target="_blank"
520+
rel="noopener noreferrer"
521+
class="inline-flex items-center text-link hover:underline"
522+
>
523+
Learn more
524+
<ExternalLinkIcon class="w-3 h-3 ml-1" />
525+
</a>
526+
</p>
527+
</Tooltip.Content>
528+
</Tooltip.Root>
529+
</Label>
530+
</div>
531+
532+
{#if form.is_replica}
533+
<div transition:slide class="space-y-4 mt-2 bg-muted p-4 rounded-md">
534+
<div class="space-y-4">
535+
<div class="space-y-2">
536+
<Label for="primary_hostname">Primary host</Label>
537+
<Input
538+
type="text"
539+
id="primary_hostname"
540+
placeholder="example.com"
541+
bind:value={form.primary.hostname}
542+
/>
543+
{#if databaseErrors.primary?.hostname}
544+
<p class="text-destructive text-sm">
545+
{databaseErrors.primary?.hostname}
546+
</p>
547+
{/if}
548+
</div>
549+
550+
<div class="space-y-2">
551+
<Label for="primary_port">Primary port</Label>
552+
<Input
553+
type="number"
554+
id="primary_port"
555+
placeholder="5432"
556+
bind:value={form.primary.port}
557+
/>
558+
{#if databaseErrors.primary?.port}
559+
<p class="text-destructive text-sm">
560+
{databaseErrors.primary?.port}
561+
</p>
562+
{/if}
563+
</div>
564+
565+
<div class="space-y-2">
566+
<Label for="primary_database">Primary database</Label>
567+
<Input
568+
type="text"
569+
id="primary_database"
570+
placeholder="postgres"
571+
bind:value={form.primary.database}
572+
/>
573+
{#if databaseErrors.primary?.database}
574+
<p class="text-destructive text-sm">
575+
{databaseErrors.primary?.database}
576+
</p>
577+
{/if}
578+
</div>
579+
580+
<div class="space-y-2">
581+
<Label for="primary_username">Primary username</Label>
582+
<Input
583+
type="text"
584+
id="primary_username"
585+
bind:value={form.primary.username}
586+
/>
587+
{#if databaseErrors.primary?.username}
588+
<p class="text-destructive text-sm">
589+
{databaseErrors.primary?.username}
590+
</p>
591+
{/if}
592+
</div>
593+
594+
<div class="space-y-2">
595+
<Label for="primary_password">Primary password</Label>
596+
<div class="relative">
597+
<Input
598+
type={showPassword ? "text" : "password"}
599+
id="primary_password"
600+
bind:value={form.primary.password}
601+
/>
602+
<button
603+
type="button"
604+
class="absolute inset-y-0 right-0 flex items-center pr-3"
605+
on:click={togglePasswordVisibility}
606+
>
607+
{#if showPassword}
608+
<EyeOff class="h-4 w-4 text-gray-400" />
609+
{:else}
610+
<Eye class="h-4 w-4 text-gray-400" />
611+
{/if}
612+
</button>
613+
</div>
614+
{#if databaseErrors.primary?.password}
615+
<p class="text-destructive text-sm">
616+
{databaseErrors.primary?.password}
617+
</p>
618+
{/if}
619+
</div>
620+
621+
<div class="flex items-center gap-2">
622+
<Switch id="primary_ssl" bind:checked={form.primary.ssl} />
623+
<Label for="primary_ssl">Primary SSL</Label>
624+
</div>
625+
</div>
626+
</div>
627+
{/if}
628+
493629
{#if poolerType}
494630
<div transition:slide>
495631
<Alert variant="default">
@@ -543,6 +679,20 @@ sequin tunnel --ports=[your-local-port]:${form.name}`;
543679
<CardTitle>Replication configuration</CardTitle>
544680
</CardHeader>
545681
<CardContent>
682+
<div class="space-y-2">
683+
<Label for="publication_name">Publication name</Label>
684+
<Input
685+
type="text"
686+
id="publication_name"
687+
bind:value={form.publication_name}
688+
/>
689+
{#if replicationErrors.publication_name}
690+
<p class="text-destructive text-sm">
691+
{replicationErrors.publication_name}
692+
</p>
693+
{/if}
694+
</div>
695+
546696
<div class="space-y-2">
547697
<div class="space-y-2">
548698
<Label for="slot_name">Slot name</Label>
@@ -553,20 +703,6 @@ sequin tunnel --ports=[your-local-port]:${form.name}`;
553703
</p>
554704
{/if}
555705
</div>
556-
557-
<div class="space-y-2">
558-
<Label for="publication_name">Publication name</Label>
559-
<Input
560-
type="text"
561-
id="publication_name"
562-
bind:value={form.publication_name}
563-
/>
564-
{#if replicationErrors.publication_name}
565-
<p class="text-destructive text-sm">
566-
{replicationErrors.publication_name}
567-
</p>
568-
{/if}
569-
</div>
570706
</div>
571707

572708
<div class="mt-8">

docker/replica-cdc-dev/README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# PostgreSQL Logical Replication Setup
2+
3+
This directory contains a Docker Compose setup for PostgreSQL logical replication with a primary and replica instance.
4+
5+
## Configuration
6+
7+
- Primary PostgreSQL runs on port `7432`
8+
- Replica PostgreSQL runs on port `7452`
9+
- Both instances use the default credentials:
10+
- Username: `postgres`
11+
- Password: `postgres`
12+
- Database: `postgres`
13+
- Replication user credentials:
14+
- Username: `replicator`
15+
- Password: `replicator_password`
16+
17+
## Connection Strings
18+
19+
### Primary Database
20+
```
21+
postgresql://postgres:postgres@localhost:7432/postgres
22+
```
23+
24+
### Replica Database
25+
```
26+
postgresql://postgres:postgres@localhost:7452/postgres
27+
```
28+
29+
## Table Structure
30+
31+
The example uses a `test_table` with the following structure:
32+
```sql
33+
CREATE TABLE test_table (
34+
id SERIAL PRIMARY KEY,
35+
name VARCHAR(100),
36+
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
37+
);
38+
```
39+
40+
## Replication Configuration
41+
42+
- Publication name: `sequin_pub`
43+
- Subscription name: `sequin_sub`
44+
- Replication slot name: `sequin_slot`
45+
46+
## Setup
47+
48+
1. Start the containers:
49+
```bash
50+
docker-compose up -d
51+
```
52+
53+
2. Wait for both containers to be healthy (you can check with `docker-compose ps`)
54+
55+
## Testing the Replication
56+
57+
### 1. Insert Data into Primary
58+
59+
Connect to the primary database and insert some test data:
60+
61+
```bash
62+
# Connect to primary
63+
docker exec -it postgres-primary psql -U postgres
64+
65+
# Once in psql, insert some test data
66+
INSERT INTO test_table (name) VALUES ('test1');
67+
INSERT INTO test_table (name) VALUES ('test2');
68+
INSERT INTO test_table (name) VALUES ('test3');
69+
```
70+
71+
### 2. Verify Data on Replica
72+
73+
Connect to the replica database and check if the data was replicated:
74+
75+
```bash
76+
# Connect to replica
77+
docker exec -it postgres-replica psql -U postgres
78+
79+
# Once in psql, verify the data
80+
SELECT * FROM test_table;
81+
```
82+
83+
### 3. Monitor Replication Status
84+
85+
To check the replication status on the primary:
86+
87+
```bash
88+
docker exec -it postgres-primary psql -U postgres -c "SELECT * FROM pg_publication;"
89+
docker exec -it postgres-primary psql -U postgres -c "SELECT * FROM pg_replication_slots;"
90+
```
91+
92+
To check the subscription status on the replica:
93+
94+
```bash
95+
docker exec -it postgres-replica psql -U postgres -c "SELECT * FROM pg_subscription;"
96+
```
97+
98+
## Cleanup
99+
100+
To stop and remove the containers and volumes:
101+
102+
```bash
103+
docker-compose down -v
104+
```
105+
106+
## Troubleshooting
107+
108+
If replication is not working:
109+
110+
1. Check if both containers are running:
111+
```bash
112+
docker-compose ps
113+
```
114+
115+
2. Check the logs:
116+
```bash
117+
docker-compose logs postgres-primary
118+
docker-compose logs postgres-replica
119+
```
120+
121+
3. Verify the replication user and permissions:
122+
```bash
123+
docker exec -it postgres-primary psql -U postgres -c "\du"
124+
```
125+
126+
4. Check if the publication and subscription are active:
127+
```bash
128+
# On primary
129+
docker exec -it postgres-primary psql -U postgres -c "SELECT * FROM pg_publication;"
130+
131+
# On replica
132+
docker exec -it postgres-replica psql -U postgres -c "SELECT * FROM pg_subscription;"
133+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
version: '3.8'
2+
3+
services:
4+
postgres-primary:
5+
image: postgres:17.4
6+
container_name: postgres-primary
7+
environment:
8+
POSTGRES_USER: postgres
9+
POSTGRES_PASSWORD: postgres
10+
POSTGRES_DB: postgres
11+
# Enable logical replication
12+
POSTGRES_INITDB_ARGS: "--data-checksums"
13+
ports:
14+
- "7432:5432"
15+
volumes:
16+
- postgres-primary-data:/var/lib/postgresql/data
17+
- ./init-primary.sh:/docker-entrypoint-initdb.d/init-primary.sh
18+
command: >
19+
postgres
20+
-c wal_level=logical
21+
-c max_wal_senders=10
22+
-c max_replication_slots=10
23+
-c hot_standby=on
24+
healthcheck:
25+
test: ["CMD-SHELL", "pg_isready -U postgres"]
26+
interval: 5s
27+
timeout: 5s
28+
retries: 5
29+
30+
postgres-replica:
31+
image: postgres:17.4
32+
container_name: postgres-replica
33+
environment:
34+
POSTGRES_USER: postgres
35+
POSTGRES_PASSWORD: postgres
36+
POSTGRES_DB: postgres
37+
ports:
38+
- "7452:5432"
39+
volumes:
40+
- postgres-replica-data:/var/lib/postgresql/data
41+
- ./init-replica.sh:/docker-entrypoint-initdb.d/init-replica.sh
42+
depends_on:
43+
postgres-primary:
44+
condition: service_healthy
45+
command: >
46+
postgres
47+
-c wal_level=logical
48+
-c hot_standby=on
49+
50+
volumes:
51+
postgres-primary-data:
52+
postgres-replica-data:

0 commit comments

Comments
 (0)