Documentation
People from your own database
Your users table, read every hour, as sign-ups.
Your users are already written down, in a users table with an address on each row and the day each
person signed up. Kicklace can read it. You connect the database with a role that can only read,
Kicklace says what's in it, you say which table and which column is which, and it lands as people with
their real sign-up dates, so your Lifecycle reads the past rather than starting today.
After that it reads again every hour. A row that appears between two reads is somebody signing up, and whatever you switched on at Signed up answers it.
Four things it never does.
- It never writes to your database. Every statement is a
select, and the role you make can't do anything else either. - It never puts anybody on a list. A row in a users table is an account, not permission to email anybody.
- It never prints the connection string back. It's locked away the moment it arrives. What the
screen and Claude can say is
app on ep-….neon.tech, and nothing more. - It never sends an email of its own.
It's under Connections → 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 matters: without it, a table you make next month is one Kicklace can't see.
2. Paste its connection string. Connections → Your database → Connect. Take the connection string Neon shows for your project and put the reader's name and password in it:
postgres://kicklace_reader:choose-a-password@ep-cool-frost-12345.eu-central-1.aws.neon.tech/neondb
Kicklace tries it with one select 1 before it saves anything, so a string that doesn't connect
is refused then and there, in your database's own words. To change it later, press Replace.
Any Postgres, or only Neon? Kicklace talks to Neon over HTTPS, and a Postgres elsewhere won't answer it. If yours is elsewhere, post your sign-ups to the events API instead, from the code that makes the account.
Saying what's in it
Ask Kicklace, in the app or from Claude, and it reads the shape of the database: every table, its columns and types, roughly how many rows, and up to three sample values per column. The samples are masked:
| What's in the column | What's 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 |
Views are left alone, and so is anything outside your own schemas. Looking and importing share one limit, sixty in ten minutes for the whole workspace.
Bringing a table in
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, and only their empty fields take what the row says. An import never overwrites something you typed.
- A row with no address is skipped and counted.
- Where you named a signed-up-at column, each person gets a sign-up on the date the row says.
- The import sends nothing. A thousand rows arriving isn't a thousand people signing up today.
- If your table marks a row as deleted (a
deleted_attimestamp, or adeletedtrue or false), name that column too, and a marked row is recorded as anaccount_deletedon that person. Nothing here is deleted with it. - A table that isn't people, like orders or projects, can come in as an object of its own instead.
A table bigger than one pass says so; running it again carries on.
The hourly read
Once a table is mapped, Kicklace reads what's new every hour: rows past the newest sign-up 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, the same signup a Clerk webhook or your own server would post.
Nothing lands twice. A row with no sign-up date that appears between two reads is dated when Kicklace read it; one that was already there when you imported the table gets no sign-up at all, because dating a thousand old accounts today would be a lie your charts repeated for a year.
When a row is deleted
If you named a deleted-at column, the hourly read looks at that too, and a newly deleted row lands as
one account_deleted on that person, the same name Clerk and Kinde post, so one side road on your
map answers all three. It's dated by the column where it holds a time, else when Kicklace read it.
Nothing here is deleted. The person stays, with their timeline, their stage, their consent records and their lists. Deleting a record is your own act (One person, however they arrive says what that takes with it).
When it stops working
A read that can't happen (a password changed, a database asleep, a table dropped) is written down: the screen says what went wrong in your database's own words, and so does your heartbeat. The next hour tries again by itself.
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.