etcd as a Postgres table? Meet etcd_fdw

PostgreSQL's extensibility is one of its greatest strengths. Foreign Data Wrappers (FDWs) are a prime example: they let Postgres query external systems as if they were local tables. With the etcd_fdw extension, built in Rust with pgrx and supabase-wrappers, you can now point Postgres directly at an etcd cluster and run CRUD operations against it.

The building blocks: etcd, FDWs, and the wrapper

etcd is a distributed key-value store focused on consistency, commonly used as a configuration store in distributed systems like Kubernetes and OpenShift. A foreign data wrapper in Postgres is a pair of C functions—a handler and a validator—that Postgres calls to plan, read, and modify data stored outside the database. The handler registers all the low-level functions that interact with the foreign source; the validator checks connection options like server strings. Together, they allow you to create a SERVER and FOREIGN TABLE, making external data appear native.

The problem etcd_fdw solves is practical. Suppose two application instances share configuration that affects database logic. Full replication is heavy-handed—you'd be syncing all data just to share a few settings. etcd_fdw offers a lighter path: etcd remains the source of truth for configuration, while Postgres reads it through a foreign table.

Why Rust and supabase-wrappers?

While pgrx is a polished framework for building Postgres extensions in Rust, FDW support isn't built in directly. The supabase-wrappers crate fills that gap with macros that turn a simple struct into a working FDW. For example, implementing the handler for a basic hashmap-backed FDW requires only this:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

[wrappers_fdw(

    version = "0.0.1",

    author = "Cybertec PostgreSQL International GmbH",

    error_type = "SimpleKvStoreError"

)]

pub(crate) struct SimpleKvStore {

    pub map: HashMap<String, String>

}

#[derive(Error, Debug)]

pub enum SimpleKvStoreError {}

impl ForeignDataWrapper<SimpleKvStore> for SimpleKvStore {

    fn new(server: ForeignServer) -> Result<SimpleKvStore, SimpleKvStoreError> {...}

    fn begin_scan(

        &mut self,

        _quals: &[Qual],

        columns: &[Column],

        _sorts: &[Sort],

        limit: &Option<Limit>,

        _options: &std::collections::HashMap<String, String>,

    ) -> Result<(), SimpleKvStoreError> {...}

    fn iter_scan(&mut self, row: &mut Row) -> Result<Option<()>, SimpleKvStoreError> {...}

    fn end_scan(&mut self) -> Result<(), SimpleKvStoreError> {...}

    fn begin_modify(

        &mut self,

        _options: &std::collections::HashMap<String, String>,

    ) -> Result<(),SimpleKvStoreError> {...}

    fn update(&mut self, rowid: &Cell, new_row: &Row) -> Result<(),SimpleKvStoreError> {...}

    fn delete(&mut self, rowid: &Cell) -> Result<(), SimpleKvStoreError> {...}

    fn end_modify(&mut self) -> Result<(), SimpleKvStoreError> {...}

}

Once the read and write logic is implemented, the foreign table behaves like any other—rows of key and value just work.

Setting up etcd_fdw

The setup follows the standard FDW pattern. Load the extension:

1

CREATE EXTENSION etcd_fdw;

Then define the wrapper and create a server that points to your etcd endpoint. After that, creating a foreign table is a single statement:

1

CREATE FOREIGN DATA WRAPPER etcd_fdw handler etcd_fdw_handler validator etcd_fdw_validator;

1

2

CREATE SERVER etcd_server foreign data wrapper etcd_fdw options (connstr '127.0.0.1:2379');

CREATE FOREIGN TABLE t_etcd_table (key text, value text) SERVER etcd_server OPTIONS (rowid_column ‘key’);

Once configured, you query etcd as if it were a normal table. Inserts and selects map directly to etcd keys and values:

1

2

INSERT INTO t_etcd_table (key, value) VALUES ('foo', 'bar'), ('bar', 'baz');

SELECT * FROM t_etcd_table ;

Which yields results like:

1

INSERT 0 2

And:

1

2

3

4

5

key | value

-----+-------

bar | baz

foo | bar

(2 rows)

Changes made outside Postgres also show up immediately. When a key is added via etcd_ctl, the foreign table reflects it without any manual refresh:

1

etcdctl put 'alice' 'bob'

1

2

3

4

5

6

7

SELECT * FROM t_etcd_table ;

  key  | value

-------+-------

alice | bob

bar   | baz

foo   | bar

(3 rows)

What this means for extension developers

The combination of pgrx and supabase-wrappers lowers the barrier for writing FDWs in a memory-safe, expressive language. Instead of hand-writing C structs and callbacks, you deal with Rust types and macros. The etcd wrapper demonstrates the pattern works for a real distributed system, not just CSV files or local test data.