Build LeaseLineage

KINDAreplaces $79/mosaves $948/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNode with Express, better-sqlite3 and server-rendered templates, bound to localhostDatesPlain calendar dates with an explicit basis, and windows stored as two datesRulesA dated rules file per jurisdiction, referenced by id from every computed windowMoneyInteger minor units with a currency code, never floating pointSendingLocal drafts, approval before any send, a suppression list checked at send time

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/leaselineage.dbEverything. Back it up; the acknowledgement log is not reconstructable.
RULES_PATHrequired./data/rulesDated rules files per jurisdiction. Versions are added, never edited.
DEFAULT_JURISDICTIONrequiredengland-and-walesUsed only when a lease does not state one, and recorded on the lease either way.
REMINDER_MAX_HORIZON_DAYSrequired540Must exceed the longest window in the rules file; a test asserts it does.
MODEL_API_KEYsecretoptionalsk-...redactedDrafting outreach copy only. Never used for anything a date depends on.
SMTP_URLsecretoptionalsmtps://user:redacted@mail.example.com:465Sending approved messages. Without it the app still drafts and tracks.
IMAP_URLsecretoptionalimaps://user:redacted@mail.example.com:993Reply polling. Without it sequences must be stopped by hand.
DAILY_SEND_CAPrequired40Across all campaigns together, not per campaign.

The build, in order

  1. 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.

    1. The original is evidence; the normalised form is the key.

      Files src/import/normalise.jstests/dedupe.test.js

    2. Someone will ask where a number came from.

      Files src/import/run.js

    3. A name and a phone number is still a prospect.

      Files src/import/run.jssrc/views/contact.ejs

    done when · tick each as it passes
    watch 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.
  2. Leases, and dates that show their working

    Every computed notice window carries the rule, the clause and the inputs that produced it.

    1. One date hides which end of the window it is.

      Files src/leases/events.jssrc/db/schema.sql

    2. Provisions differ across the UK and they change.

      Files data/rules/uk.jsondocs/rules.md

    3. If 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 passes
    watch 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.
  3. Reminders that fire while the window is still open

    The horizon is longer than the longest statutory window and acknowledgement is recorded.

    1. The law sets the horizon; your default does not.

      Files src/reminders/schedule.jstests/horizon.test.js

    2. It is the answer to the only question that gets asked afterwards.

      Files src/reminders/ack.js

    3. That is where the person is when they decide to act.

      Files src/views/reminder.ejs

    done when · tick each as it passes
    watch 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.
  4. Outreach that stays a draft until you read it

    Sequences are generated, approved by a person, throttled, and stopped by a reply.

    1. Approval happened in the past; the list is now.

      Files src/send/guard.jstests/suppression.test.js

    2. Nobody should be able to say the machine sent it.

      Files src/campaigns/approve.js

    3. Per-campaign caps add up to a volume you did not intend.

      Files src/send/limits.js

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more automation.