Privacy policy

Last updated 13 September 2026. Applies to Duly version 1.0.

Duly is a Google Sheets™ editor add-on from Kehr & Co., built and supported by one person. This policy says exactly what Duly reads, what leaves your spreadsheet, what is stored, for how long, and how to have it deleted. It was written from the source code rather than from a template.

The short version: Duly runs inside your spreadsheet and sends email from your own Google account. The contents of your spreadsheet never reach any server of mine. The only thing the add-on ever sends to Duly’s own server is a one-way hash of the email address someone typed when linking a purchase — and, if you use the support form, the message you wrote and the address you asked for a reply at.

If anything here does not match what Duly actually does, that is a bug and I want to know: hello@kehrandco.com.

1. Who is responsible

Dylan Kehr, doing business as Kehr & Co., operating from Georgia, United States. Duly has one developer, who also answers support. Contact: hello@kehrandco.com. There is no support team, no advertising, no data broker, and no third party with a claim on anything described below.

2. The permissions Duly asks for, and what each one is for

Google shows you five permissions when you install Duly. Those five are all it has. They are pinned by a test in the source, and a sixth would require a new review by Google before it could reach you.

Permission What Duly does with it
.../auth/spreadsheets.currentonly Reads only the columns your rule names, in the one spreadsheet Duly is open in, and writes its own Duly log, Duly log rows and Duly state sheets there. This is the narrow, single-document form of the Sheets permission on purpose: it gives Duly no access to any other file in your Drive, and no access to Drive itself.
.../auth/script.send_mail Sends the reminders and the watchdog notices, from your own Google account address, through Google’s mail service. This is the one permission Google classifies as sensitive. It can only send; it gives Duly no ability to read, search or delete any mail, and Duly asks for no Gmail permission that would.
.../auth/script.scriptapp Creates and removes the hourly time-driven trigger, so your dates are checked when you are not there. Without it Duly could only run while you had the sidebar open.
.../auth/script.container.ui Draws the sidebar and the Extensions menu inside Google Sheets.
.../auth/script.external_request Makes exactly three kinds of request, all of them to duly-licence.dylankehr.workers.dev: the licence lookup, the Team seat roster, and the support form. A spreadsheet with no purchase linked and no support message sent makes no external request at all.

Duly asks for no Gmail permission, no Drive-wide permission, no Calendar permission and no full-Sheets permission.

Google API Services Limited Use

Duly’s use and transfer of information received from Google APIs to any other app will adhere to the Google API Services User Data Policy, including the Limited Use requirements. Duly does not transfer Google user data to anyone, does not use it for advertising, does not sell it, and does not allow any human to read it — except where you have sent it to me yourself through the support form, or where reading it is necessary for security purposes or to comply with applicable law.

3. What stays inside your spreadsheet

Almost everything Duly holds, it holds inside your own spreadsheet, or in Google’s per-document and per-user property storage attached to it. None of the following is transmitted anywhere by Duly.

  • Your rules. The tab name, the header row, the column headers you picked, the offsets, any fixed recipient addresses you typed, the stop-condition values, the subject and body templates, the send hour and the time zone. Stored in Google’s document properties for that spreadsheet.
  • Duly state, a hidden sheet. One row for each reminder already sent, so that none is ever sent twice: a key for the spreadsheet row, and when it was sent. The key is either the value of a column you have named Duly ID, or a hash of the row’s date and its first non-empty text cell. Entries older than 400 days are pruned automatically.
  • Duly log, a visible sheet. One row per run: when, which rule, one outcome word, rows scanned, sent, skipped, failed, and a detail line.
  • Duly log rows, a visible sheet. One row per row outcome: when, rule, row number, row key, outcome, offset, recipients and reason. Entries older than 90 days are removed automatically. This sheet is deliberately plain, sortable and exportable — it is what you use to prove a particular message went out.
  • The plan cache. If a purchase has been linked, the hash described in section 5 and the last answer received for it, in your own per-user property storage, for up to 24 hours.

You can delete every bit of that yourself, without an account and without asking me: delete the rules from the sidebar, delete the three sheets, and remove the add-on from the spreadsheet.

4. The email Duly sends

Reminders are sent by Google, from your own Google account, using Google’s mail service for Apps Script. They go to the recipients your rule resolves: the fixed addresses you typed, and, if your rule uses an address column, the addresses in that column of the matching rows. Watchdog notices — the message that says Duly has not run for a while — go the same way, to the spreadsheet’s owner.

I never see those messages, their recipients or their contents, and they do not pass through any server of mine. The send happens inside Google. Because the mail is from your own address, replies come back to you. Delivery, storage and logging of that mail are Google’s, under whatever terms govern the Google account that sent it. It counts against that account’s daily Apps Script mail quota.

5. What the add-on sends to Duly’s own server

Duly has one server: a Cloudflare Worker at duly-licence.dylankehr.workers.dev, backed by a Cloudflare D1 database. It exists to answer the question “has this person bought a licence”, and for nothing else. No part of the reminder engine runs there.

Only a hash of the address is sent; the add-on never transmits sheet contents, cell values, or recipient addresses.

That sentence is quoted verbatim from Duly’s own specification, section 12, and the three paragraphs below are what it means in the code.

The licence lookup

When you link a purchase, you type the Google account email you bought with. The add-on lower-cases it, takes its SHA-256 digest, keeps the 64-character hexadecimal digest and discards the address itself. Only that digest is sent: GET /v1/licence?e=<digest>. The reply is one word (free, pro or team) and a date. The lookup runs at most once a day. If no purchase has been linked, no request is made at all — the free tier needs no account, no sign-up and no email address.

Team seats

If you bought Team, the Plan screen sends the same kind of digest for the buyer and for each person given a seat. Because the server has never held an address, a seat roster cannot show a name or an address; a seat is listed by the day it started and by an identifier the server invented.

What the database can hold

The licence table has exactly five columns: the email hash, the plan word, the paid-through date, Stripe’s customer identifier, and when the row was last written. The seat table has a seat identifier, two hashes and a date. There is no column anywhere in the database into which a cell value, a recipient address, a column header, a rule name or a sheet name could be put. That is structural, not a promise about behaviour: it is not that Duly chooses not to store your spreadsheet, it is that there is nowhere to put it.

6. The support form

If you use “Email support” in the sidebar, exactly this is sent:

  • the reply address you type into the form;
  • the message you type;
  • your plan, as one word;
  • how many rules this spreadsheet has, as a number;
  • the last three rows of the run log, reduced to a date, one outcome word and four counts.

The run log’s rule-name and detail columns are dropped before sending, because those can contain your own words. Nothing else about the spreadsheet is attached, and the sidebar lists this before you type anything.

The message is stored in the same D1 database and delivered to my own Google mailbox — the one hello@kehrandco.com forwards to — by a small Apps Script web app that I own, which emails it and stores nothing itself. Your reply address is set as the Reply-To. If delivery fails, the stored message is retried until it succeeds, so a message cannot be quietly lost. Your message and your reply address then live in my ordinary email, and in the database, until deleted — see section 8.

7. Payment

Purchases are handled by Stripe. Stripe collects your name, email address, billing address and card details and processes the payment. I never see or handle card details, and Duly has no code that touches them. Stripe’s handling of that information is governed by Stripe’s privacy policy.

Stripe then tells Duly’s Worker, over a signed webhook, that a subscription was bought, renewed or cancelled, and for which email address. The Worker hashes that address the same way the add-on does, stores the hash, and does not store the address. Duly’s own database therefore holds no customer email address, no name and no billing address: the record that identifies you as a customer lives inside Stripe, where the payment is.

8. How long things are kept, and how to have them deleted

What Where How long
Rules, sent-state, run log your spreadsheet until you delete them; Duly state prunes itself after 400 days, Duly log rows after 90
Plan cache and licence hash your Google per-user properties cache 24 hours; hash until you unlink or remove the add-on
Licence row: hash, plan, date, Stripe customer id Cloudflare D1 while the licence exists, and until you ask for it to be deleted
Team seat rows: seat id, two hashes, a date Cloudflare D1 until the seat is removed, or you ask for deletion
Stripe webhook event ids Cloudflare D1 kept so that a redelivered payment event cannot be counted twice; they name an event, not a person
Support messages and reply addresses Cloudflare D1, and my email until you ask for deletion
Payment records Stripe under Stripe’s policy and the record-keeping law that applies to them

To have your data deleted, email hello@kehrandco.com and say so. I will delete the licence row, any seat rows and any support messages, and write back to confirm, within 30 days and usually within a few days. Note that deleting a licence row before its paid period has ended means the add-on will treat that account as free; if a refund is what you want, ask for it in the same message.

Because the database holds only a one-way hash, I cannot look you up without being told the address. That is the point of the design, but it does mean a deletion or access request has to come from, or name, the address concerned.

Access and correction. There is little to export — a hash, a word and a date — but ask and I will send you exactly what is stored against your address, and correct anything wrong.

9. Legal basis, if you are in the UK or EU

  • Contract. The licence hash, plan and date exist to give you the paid features you bought.
  • Legitimate interests. The support message you send, so I can answer it; and ordinary server logs, so the service can be operated and defended.
  • Legal obligation. Records Stripe and I have to keep about payments.

There is no processing based on consent because there is nothing optional being collected, no advertising, and no profiling or automated decision-making of any kind.

10. Where the data is

  • Cloudflare — the Worker, the D1 database, and the email routing that forwards mail sent to hello@kehrandco.com to my own mailbox.
  • Google — your spreadsheet, the add-on’s execution, and the mail it sends.
  • Stripe — payment.
  • Google (my own mailbox) — support correspondence.

Duly is operated from Georgia, United States, so using it involves an international transfer if you are outside the United States. Cloudflare, Google and Stripe each publish their own transfer safeguards.

11. Server logs, and this website

Cloudflare records ordinary request metadata for the Worker — address, time, path, status code — as any web server does; the licence path carries the hash in the URL. I do not use those logs for analytics, do not join them to anything, and have built no reporting on them. Google records execution errors from the add-on in the Cloud project the add-on is published from; those are error messages and stack traces, not a copy of your data.

The website you are reading sets no cookies, runs no JavaScript at all, loads nothing from any third party, and has no analytics or tracking of any kind. Its content security policy forbids scripts outright.

12. Children

Duly is a tool for work and is not directed at children. Do not use it if you are under 13, or under the age of digital consent where you live.

13. Changes to this policy

If this policy changes in a way that matters, the date at the top changes and the change is listed on the changelog. A change that widened what Duly collects would also require a new permission prompt from Google, which you would see and have to accept before it took effect.

14. Complaints

Write to hello@kehrandco.com first; I answer support within two business days. If you are in the UK or the EU and are not satisfied, you may complain to your local data protection supervisory authority.