Build LeaseLineage
KINDAreplaces $79/mosaves $948/yrback to the verdict
A broker's workspace whose hard problems are computing statutory and contractual notice windows that a person can audit, reminding far enough ahead that a twelve-to-six-month window is still open, and keeping outreach as drafts a human approves.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the workspace and the mailbox poller.
Get it nodejs.org or your version manager. open ↗
Verify
node --version prints v22 or higher - have readyfree
Why Every computed window points at a row in it, and a rule with no statute reference and no version date is a guess wearing a date's clothing.
Get it Work through the provisions that apply to the leases you actually handle, record statute, section, version date and the computation, and cite the source for each. open ↗
Verify
Every rule row has a statute, a section and a version date, and a test asserts it - have readyfree
Why Window computation looks obvious until it meets a lease whose commencement and term end disagree with the rent review cycle.
Get it Take leases you already manage and reduce each to the dates and the clause references.
Verify
Each produces windows whose 'why this date' panel matches what you would have worked out by hand - API keyfree
Why Reply detection is what stops a sequence, and without it the software keeps emailing a person who already answered.
Get it Enable an application password on the mailbox you send from.
Verify
A reply to a sent message stops the sequence within one polling interval - decidefree
Why A tool that computes legal dates invites reliance, and the boundary needs to be written down before the first reminder fires rather than after a missed one.
Get it Write down that the app computes windows from a dated rules file, that it never advises, and what the user is expected to confirm independently.
Verify
The same text appears on every reminder and in the README
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- organisations(id, name, kind, market, notes) and contacts(id, organisation_id, name, title, email, phone, source, unsubscribed_at) - properties(id, address, asset_class, owner_org_id, area_sqft, notes) - leases(id, property_id, tenant_org_id, jurisdiction, commencement, term_end, contracted_out, rent_minor, rent_currency, rent_period, notes) - lease_events(id, lease_id, kind, target_date, basis, clause_ref, rule_id, inputs_json, window_opens, window_closes) · kind covers break, rent review, expiry and statutory notice - rules(id, jurisdiction, statute, section, version_date, description) · every computed window names one of these rows - reminders(id, lease_event_id, fire_on, tier, acknowledged_at, acknowledged_by, note) · acknowledgement is recorded because 'nobody told me' is the argument you are preventing - campaigns, sequence_steps and messages(id, contact_id, step_id, state, body, approved_by, approved_at, sent_at, provider_id) · state is draft until a person approves it - No lease event stores a single deadline. It stores an opening date, a closing date, the rule that produced them and the inputs, so a disagreement is a lookup rather than an argument.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/leaselineage.db | Everything. Back it up; the acknowledgement log is not reconstructable. |
RULES_PATH | required | ./data/rules | Dated rules files per jurisdiction. Versions are added, never edited. |
DEFAULT_JURISDICTION | required | england-and-wales | Used only when a lease does not state one, and recorded on the lease either way. |
REMINDER_MAX_HORIZON_DAYS | required | 540 | Must exceed the longest window in the rules file; a test asserts it does. |
MODEL_API_KEYsecret | optional | sk-...redacted | Drafting outreach copy only. Never used for anything a date depends on. |
SMTP_URLsecret | optional | smtps://user:redacted@mail.example.com:465 | Sending approved messages. Without it the app still drafts and tracks. |
IMAP_URLsecret | optional | imaps://user:redacted@mail.example.com:993 | Reply polling. Without it sequences must be stopped by hand. |
DAILY_SEND_CAP | required | 40 | Across all campaigns together, not per campaign. |
The build, in order
Organisations, contacts and an import that does not duplicate
A CSV of prospects lands as clean rows and a second import of the same file changes nothing.
The original is evidence; the normalised form is the key.
Files
src/import/normalise.jstests/dedupe.test.jsSomeone will ask where a number came from.
Files
src/import/run.jsA name and a phone number is still a prospect.
Files
src/import/run.jssrc/views/contact.ejs
done when · tick each as it passeswatch out- Deduplicating on the raw email string lets the same person in three times because one export capitalised the local part and another added a trailing space, and by the time you notice, all three are in a campaign and have had the same message twice. Normalise before comparing, keep the original string as well, and make the second import of an identical file assert zero new rows in a test.
Leases, and dates that show their working
Every computed notice window carries the rule, the clause and the inputs that produced it.
One date hides which end of the window it is.
Files
src/leases/events.jssrc/db/schema.sqlProvisions differ across the UK and they change.
Files
data/rules/uk.jsondocs/rules.mdIf it has to recompute to explain itself, it cannot explain an old answer.
Files
src/views/why.ejstests/why.test.js
done when · tick each as it passeswatch out- Storing a single deadline date is the mistake the whole application is built around avoiding, because the legally interesting thing is a window with two ends, and a user who sees one date assumes it is the last day when it may well be the first. Store both ends, label them in the interface, and refuse to render any computed date that has no rule id attached.
Reminders that fire while the window is still open
The horizon is longer than the longest statutory window and acknowledgement is recorded.
The law sets the horizon; your default does not.
Files
src/reminders/schedule.jstests/horizon.test.jsIt is the answer to the only question that gets asked afterwards.
Files
src/reminders/ack.jsThat is where the person is when they decide to act.
Files
src/views/reminder.ejs
done when · tick each as it passeswatch out- Defaulting the reminder horizon to thirty or ninety days, which is what every other reminder feature does, guarantees that a window opening twelve months before a termination date has already closed by the time anything fires, and the software will have been quietly useless for the one thing it existed to do. Derive the longest tier from the rules file rather than from a constant, and assert in a test that no window can close before its first reminder fires.
Outreach that stays a draft until you read it
Sequences are generated, approved by a person, throttled, and stopped by a reply.
Approval happened in the past; the list is now.
Files
src/send/guard.jstests/suppression.test.jsNobody should be able to say the machine sent it.
Files
src/campaigns/approve.jsPer-campaign caps add up to a volume you did not intend.
Files
src/send/limits.js
done when · tick each as it passeswatch out- Checking the suppression list when the sequence is generated means a person who unsubscribes on Tuesday still gets Wednesday's step, because the message was approved on Monday and nothing looked again. Check at the moment of sending, against the live list, and write a test that unsubscribes a contact between approval and send and asserts nothing left the building.
The record you will be asked forproduct builder
Everything exports, nothing important is deleted, and a restore is something you have done.
It is the only way an old date can be explained later.
Files
src/export.jsdata/rules/uk.jsonThe messages and acknowledgements outlive the contact record.
Files
src/db/delete.jsA backup nobody has restored is a hope.
Files
tests/restore.test.jsscripts/backup.sh
done when · tick each as it passeswatch out- Exporting the computed window without the rules file version means that a year later, after the rules file has been corrected, nobody can reproduce the date the software actually showed, and the only remaining evidence of what the broker was told is gone. Stamp the rule id and the rules file version into both the stored event and the export, and keep old rule versions in the file rather than editing them in place.
That is the whole plan for LeaseLineage. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Commercial contact data enrichment.
- Drafting or serving notices.
- Mailbox provider authorisation at scale.
- Any claim that a computed date is correct in law.
- Apollo and Hunter prospect data
- Gmail and Outlook OAuth and reply sync
- managed sending and deliverability
- brokerage team workspace
- custom sending domains
- hosted compliance and suppression operations
- A portfolio calendar view of every open window across all leases, sorted by closing date
- A second jurisdiction's rules file, which is the real test of whether the rule abstraction works
- A printable lease summary that carries the clause references and the inputs alongside each date
Need the files? The project pack on the verdict page hands your agent the whole brief · more automation.