Skip to content

Documentation

People from your own database

Your users table already holds your people and the day each of them signed up. Connect the database with a role that can only read and Kicklace says what is in it, brings the table in as people with their real signup dates, and reads what is new every hour — so a row that appears tomorrow is a sign-up your automations answer. It never writes to your database and it puts nobody on a list.

Your users are already written down. They are in a users table with an address on each row and the day that person signed up beside it, and typing all of that into a CRM again is the reason most small software companies never have one.

So Kicklace can read it. You connect the database with a connection string for a role that can only read, Kicklace says what is in it in plain words, you say which table is worth bringing in and which column is which, and it lands as people with their signup dates — the real ones, so the lifecycle and the charts read the past rather than starting today. After that it keeps reading, every hour: a row that appears in your users table between two passes is somebody signing up, and it fires whatever your automations answer a sign-up with.

Four things it will never do.

  • It never writes to your database. Every statement Kicklace sends is a select. There is no insert, no update, no schema change, and the role you make cannot do any of those either, so the promise is kept at both ends rather than only at ours.
  • It never puts anybody on a list. A row in a users table is an account, not permission to email anybody. What those people agreed to is not in that table, so there is nothing to keep as the proof — people imported this way are on no list until they join one.
  • It never prints the connection string back. It is encrypted the moment it arrives, the same way every other secret here is. What the screen and Claude say is app on ep-….neon.tech, and that is all either of them can say: even a failure from your database has anything string-shaped taken off it before anybody reads it.
  • It never sends an email of its own. Bringing people in is not writing to them.

The screen is Settings → Integrations → People from your database, and only an owner or an admin can connect or disconnect one.


Setting it up

1. Make a role that can only read. Open your project's SQL editor at Neon and run these, changing neondb to your database's name and choosing a password:

create role kicklace_reader with login password 'choose-a-password';
grant connect on database neondb to kicklace_reader;
grant usage on schema public to kicklace_reader;
grant select on all tables in schema public to kicklace_reader;
alter default privileges in schema public
  grant select on tables to kicklace_reader;

The last one is worth having: without it a table you make next month is one Kicklace cannot see, and the only sign of it is a table missing from a list.

2. Paste its connection string. Take the connection string Neon shows for your project and put the reader's name and password in place of the ones in it:

postgres://kicklace_reader:choose-a-password@ep-cool-frost-12345.eu-central-1.aws.neon.tech/neondb

Kicklace opens it and asks it for one select 1 before it saves anything. A string that does not connect is refused there and then, with your database's own words in the sentence — because finding that out a week later, from an hourly read that has quietly failed every hour since, is no use to anybody.

Then it is stored encrypted and never shown again. To change it, press Replace and paste the new one.

Any Postgres, or only Neon? The driver Kicklace uses is Neon's own, which speaks to Neon over HTTPS. A Postgres somewhere else will not answer it. If yours is elsewhere, post your sign-ups to the events API instead — one call from the code that makes the account, which is better anyway, because it knows the moment it happens.

Saying what is in it

Ask Kicklace — in the app, or from Claude — and it reads the shape of the database: every table with its columns, their types, roughly how many rows each holds, and up to three sample values per column.

The samples are what make a mapping obvious, so they are real values, and that is exactly why they are not printed as they are:

What is in the column What is printed
greg@northshelf.com g***@example.com — enough to say "this column is addresses"
sk_live_9f2b…, a hash, a UUID, a signed link [token]
Somebody's whole support message the first sixty characters, then
A number, a date, a plan name, a country exactly what it is

Row counts are Postgres's own estimates, which is why they come out round. Nothing outside your own schemas is looked at, and a view is left alone: a view is a query rather than data, and importing one would bring in whatever it happens to select today.

Bringing a table in

You say which column is the address, which is the name, which is the day the row was made, and which others are worth keeping. Then:

  • People are matched by address. The rule is fill blanks, never overwrite: somebody who is already here keeps every value they have and only their empty fields take what the row says, so an import can never quietly replace something you typed.
  • A row with no address is skipped and counted, never given one.
  • Where you named a signed-up-at column, each person gets a signup event on the date the row says. That is what moves them to Account and what a chart of sign-ups by month reads.
  • The import fires no automations. A thousand rows arriving is not a thousand people signing up, however it looks to a trigger, so nothing is sent and nothing runs.
  • A table that is not people — orders, projects, releases — can come in as an object of its own instead. Those are matched by name, and they get no signup events, because a purchase order does not have a lifecycle.

A table bigger than one pass can read hands back what it did and says so; running it again carries on from where it stopped.

The hourly read

Once a table is mapped, Kicklace reads what is new in it every hour: rows past the newest signup date it has seen, or past the largest primary key where the table has no date.

A row that arrives this way is somebody signing up, so this is the half that fires — the same signup event a Clerk webhook or your own server would post, which means "when somebody signs up, send the welcome" answers a row in your database without knowing your database exists.

Nothing lands twice. The cursor is deliberately inclusive — it reads the newest instant again rather than risk missing somebody who signed up in the same second — and the import fills blanks while the event carries a key of its own, so re-reading a row costs one query and changes nothing.

Two things it cannot see, and says so rather than guessing: a row whose signup date is empty (there is nothing on it to say it is new), and a row that was deleted over there. Deleting an account at your end is not consent to erase what your CRM knows about that person, so nothing here removes a record; you delete one from their own page, and Privacy says what that takes with it.

When it stops working

A read that cannot happen — a password changed, a database asleep, a table dropped — is written down rather than thrown: the frame says what went wrong in your database's own words, and the same line is one row in Settings → Your website → heartbeat, where every post from outside lands. Nothing is retried in a hurry; the next hour tries again by itself, and the moment it works the line clears.

Disconnecting

Press Disconnect. The string goes, the mapping goes, and nothing is read again.

Everybody who came in through it stays exactly where they are, with their timeline, their signup dates and their stage. Where Kicklace first heard of somebody is not a reason to forget them.