Vibecode IFTTT
track this build5 phases, 15 steps, beginner friendly0%For a few personal applets, a script or simple webhook worker is enough; the full consumer integration catalog and smart-home breadth are what you lose.
You are building a lean indie version of IFTTT. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # IFTTT · indie build Rules connecting a trigger to an action: polled sources with change detection or inbound webhooks, an event key that makes each event fire exactly once, a per-rule rate ceiling that stops a runaway, a run log showing what happened, and secrets that live outside the rule definitions. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite | a few dozen rules and a few thousand runs a month | | Triggers | Scheduled polls with change detection, plus inbound webhooks | most services offer one or the other and both need different idempotency handling | | Idempotency | An event key per trigger, unique in the database | the single mechanism that makes exactly-once possible | | Actions | Outbound requests with retries and a per-rule ceiling | a retry that is not idempotent is a second side effect | | Secrets | Stored outside rule definitions, referenced by name | rules get exported and shared, and a rule containing a token is a leaked token | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the service. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to run it continuously** · 5 to 10 US dollars a month, or free on hardware you own - Why: Automations that only run while your laptop is open miss events and then fire a burst, which is worse than not running. - Get it: A small virtual server or a machine at home that stays on. This workload is tiny. - Verify: The service runs on a schedule and survives a reboot - [ ] **An HTTPS endpoint for inbound webhooks** · roughly 10 to 15 US dollars a year for a domain - Why: Phase 4 needs one, and most services will not deliver to plain HTTP. - Get it: A domain pointed at your server with automatic certificates, or a tunnel for development. - Verify: A test delivery reaches your endpoint over HTTPS - [ ] **Credentials for two real services** · free for most personal API access - Why: One trigger source and one action target, both real. A test harness will not have the rate limits, pagination and redelivery behaviour that shape this build. - Get it: API keys or tokens for two services you actually use, scoped as narrowly as they allow. - Verify: A rule polls one and acts on the other successfully - [ ] **An understanding of which rules could loop** · free - Why: Phase 2's ceiling exists because a loop with external side effects is the expensive failure, and knowing which of your rules could form one determines how tight the ceiling should be. - Get it: Before enabling any rule, write down whether its action could cause its own trigger, directly or through another rule. - Verify: Each rule has a loop assessment recorded ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. - consumer integration catalog - smart-home integrations - mobile applets - easy UI - maintenance when APIs change If one of those is essential to you, that is the reason to keep paying for IFTTT, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · IFTTT The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me small automations connecting services you use, that do not fire twice and cannot run away to replace IFTTT. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two failure modes define this build and both are worse than the automation is valuable. The first is firing twice for one event, which sends a duplicate message or creates a duplicate record, and it happens whenever a poll overlaps or a webhook is redelivered. The second is a loop: an action that causes a trigger that causes the action. A loop with an external side effect can send hundreds of messages before you notice. So idempotency and a rate ceiling are Phase 2, before any useful automation exists. Both are cheap to build first and expensive to retrofit after an incident. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite · a few dozen rules and a few thousand runs a month - Triggers: Scheduled polls with change detection, plus inbound webhooks · most services offer one or the other and both need different idempotency handling - Idempotency: An event key per trigger, unique in the database · the single mechanism that makes exactly-once possible - Actions: Outbound requests with retries and a per-rule ceiling · a retry that is not idempotent is a second side effect - Secrets: Stored outside rule definitions, referenced by name · rules get exported and shared, and a rule containing a token is a leaked token ### Data model (create this before Phase 1) - rules(id, name, enabled, trigger_kind, trigger_config_json, action_kind, action_config_json, max_runs_per_hour) - events(id, rule_id, event_key, payload_json, observed_at, state) · unique on rule and event key, which is the idempotency - runs(id, event_id, started_at, finished_at, state, attempts, request_summary, response_summary, error) - secrets(name, value_encrypted) · referenced from configs by name, never inlined - cursors(rule_id, value, updated_at) · what a polled trigger has already seen - The unique constraint on rule and event key is what makes exactly-once real rather than intended. ### Phase 1 · One trigger, one action, logged Build: A rule with a scheduled poll and an outbound request action. Log every run with a summary of the request and the response. Redact secret values from the log. Nothing clever yet. Trap: Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. Done when: A rule polls, fires and records a run with request and response summaries. Secret values are absent from the log, verified by grepping for a known token. A failing action records the error rather than disappearing. Disabling a rule stops it firing. Do not build yet: No idempotency, rate limits or webhooks. ### Phase 2 · Exactly once, and a ceiling Build: Derive an event key per trigger and enforce uniqueness in the database. A per-rule limit on runs per hour that disables the rule and notifies you when exceeded. Test both by causing them deliberately. Trap: Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. Trap: A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. Done when: The same event observed twice produces one run, enforced by a database constraint rather than a check. A deliberately looping rule hits its ceiling, disables itself and notifies. Overlapping polls do not double fire, verified by running two concurrently. A disabled-by-ceiling rule requires deliberate re-enabling. Do not build yet: No webhooks or secret management yet. ### Phase 3 · Polling politely with cursors Build: Store a cursor per rule and request only what is new. Use conditional requests where the source supports them. Back off on errors and on rate limit responses. Handle a cursor the source no longer accepts. Trap: Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. Done when: A poll requests only items after the cursor. An expired or rejected cursor triggers a documented recovery rather than an error. Backoff increases on repeated failure and respects a retry hint. A source producing more items than one page returns is paged through rather than truncated. Do not build yet: No webhooks or sharing yet. ### Phase 4 · Inbound webhooks Build: An inbound endpoint per rule with an unguessable path. Verify a signature where the source provides one. Derive the event key from the payload's own identifier so a redelivery deduplicates. Respond quickly and process asynchronously. Trap: Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. Done when: A webhook delivery creates an event and returns quickly, verified by timing the response. Redelivering the same payload produces one run. A payload with a bad signature is rejected. An unguessable path is required and a wrong path returns nothing informative. Do not build yet: No secret management or sharing yet. ### Phase 5 · Secrets outside the rules Build: Store secrets separately, encrypted, referenced from configs by name. Exporting a rule exports the reference rather than the value. Importing prompts for the secrets it needs rather than failing obscurely. Trap: A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. Done when: A rule config contains a secret reference and no value, verified by inspecting the export. Importing a rule lists the secrets it needs and prompts for them. A missing secret produces a clear error naming it rather than an authorisation failure from the target service. Secrets are encrypted at rest. Do not build yet: Operations come last. ### Phase 6 · Running it continuously Build: Run as a service surviving restarts. Alert on a rule that has not fired in its expected window, on repeated failures and on a ceiling trip. Back up the database. Report per-rule run counts and failure rates. Trap: A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here. Done when: The service survives a restart with cursors intact. A rule that stops firing within its expected window produces an alert. Repeated failures alert rather than retrying indefinitely. A restore reproduces rules, cursors and secrets, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - The consumer integration catalogue, which is hundreds of maintained connectors and is the entire product. - Smart home integrations, which need device protocols and local network presence. - Mobile applets and the triggers that come from a phone's sensors and location. - Someone else maintaining a connector when a service changes its API, which now happens to you without warning. ### README must contain - That idempotency is enforced by a database constraint rather than a check, and why - That a per-rule ceiling disables a runaway rather than warning about it - That webhooks acknowledge immediately and process asynchronously - That secrets are referenced by name and never appear in an exported rule - That rules have an expected frequency, because silence is the failure mode ===== AGENTS.md ===== # Agent instructions · IFTTT indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with SQLite, Scheduled polls with change detection, plus inbound webhooks, An event key per trigger, unique in the database, Outbound requests with retries and a per-rule ceiling, Stored outside rule definitions, referenced by name. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. ===== BUILD_PLAN.md ===== # Build plan · IFTTT Rules connecting a trigger to an action: polled sources with change detection or inbound webhooks, an event key that makes each event fire exactly once, a per-rule rate ceiling that stops a runaway, a run log showing what happened, and secrets that live outside the rule definitions. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One trigger, one action, logged A rule fires and you can see exactly what it sent and what came back. ### Steps 1. Model rules with configs Trigger and action as data, so a rule is inspectable rather than code. Files: `src/rules.js` 2. Log runs with summaries Enough to debug, redacted at the boundary. Files: `src/log.js` 3. Redact structurally A logging function that cannot emit a secret value, rather than discipline. Files: `src/redact.js` ### Done when - [ ] A rule fires and records request and response summaries - [ ] No secret value appears in the log - [ ] A failing action records its error - [ ] Disabling stops the rule firing ### Watch out - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. ## Phase 2 · Exactly once, and a ceiling One event fires one action, and a runaway rule stops itself. ### Steps 1. Derive and enforce event keys Unique on rule and key, in the database. Insert failure means duplicate. Files: `src/events.js`, `migrations/` 2. Add a per-rule ceiling that disables Not a warning. A loop with side effects is the case this exists for. Files: `src/limits.js` 3. Cause both deliberately A loop and a concurrent double poll. Untested safety is not safety. Files: `test/idempotency.test.js` ### Done when - [ ] A repeated event produces one run, enforced by constraint - [ ] A looping rule disables itself and notifies - [ ] Concurrent polls do not double fire - [ ] Re-enabling after a ceiling trip is deliberate ### Watch out - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. ## Phase 3 · Polling politely with cursors A polled source is checked without refetching everything and without missing items. ### Steps 1. Store and advance a cursor After a successful batch, not before. A cursor advanced on failure loses events. Files: `src/poll.js` 2. Use conditional requests where available Cheaper for you and politer to the source. Files: `src/http.js` 3. Page rather than truncate If a poll returns a full page, there may be more. Truncating loses events silently. Files: `src/poll.js` ### Done when - [ ] A poll requests only items after the cursor - [ ] A rejected cursor triggers documented recovery - [ ] Backoff increases and respects a retry hint - [ ] A full page is paged through rather than truncated ### Watch out - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. ## Phase 4 · Inbound webhooks A service can push to you, and a replayed delivery does not fire twice. ### Steps 1. Acknowledge fast, process later Queue and return. Slow processing inside the request causes redelivery. Files: `src/webhook.js` 2. Verify signatures where available And where not, rely on the unguessable path and say so in the documentation. Files: `src/webhook.js` 3. Key on the payload's own identifier So a redelivery deduplicates naturally. Files: `src/events.js` ### Done when - [ ] A delivery returns quickly and is processed asynchronously - [ ] A redelivery produces one run - [ ] A bad signature is rejected - [ ] A wrong path returns nothing informative ### Watch out - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. ## Phase 5 · Secrets outside the rules A rule can be exported and shared without leaking a token. ### Steps 1. Reference secrets by name Configs never contain values. Grep an export to confirm. Files: `src/secrets.js` 2. Prompt on import List what is needed. A rule that fails with a service authorisation error is hard to diagnose. Files: `src/import.js` 3. Encrypt at rest With the key outside the database. Losing it loses the secrets, which is the right failure. Files: `src/crypto.js` ### Done when - [ ] An exported rule contains no secret value - [ ] Import prompts for required secrets - [ ] A missing secret produces a clear named error - [ ] Secrets are encrypted at rest ### Watch out - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. ## Not in this build - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. ## After v1, if you want it - A filter step between trigger and action, which removes most of the need for more rules - A dry run mode showing what a rule would do without doing it - Templating action payloads from trigger data, which is where rules become genuinely useful ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, events, runs and cursors. DB_PATH=./data/rules.db # Required · secret. Encrypts the secret store. Kept outside the database; losing it loses the secrets deliberately. SECRETS_KEY=generate-a-long-random-value # Required. Default ceiling per rule. Exceeding it disables the rule rather than warning. DEFAULT_MAX_RUNS_PER_HOUR=30 # Required. Where inbound endpoints live. Must be HTTPS. WEBHOOK_BASE_URL=https://hooks.example.com # Required. Floor on polling frequency. Politeness to the sources and a limit on your own request volume. POLL_MIN_INTERVAL_SECONDS=300 # Required · secret. Where ceiling trips, failures and silent rules are reported. ALERT_WEBHOOK=https://chat.example.com/hook
You are building a lean indie version of IFTTT. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # IFTTT · indie build Rules connecting a trigger to an action: polled sources with change detection or inbound webhooks, an event key that makes each event fire exactly once, a per-rule rate ceiling that stops a runaway, a run log showing what happened, and secrets that live outside the rule definitions. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite | a few dozen rules and a few thousand runs a month | | Triggers | Scheduled polls with change detection, plus inbound webhooks | most services offer one or the other and both need different idempotency handling | | Idempotency | An event key per trigger, unique in the database | the single mechanism that makes exactly-once possible | | Actions | Outbound requests with retries and a per-rule ceiling | a retry that is not idempotent is a second side effect | | Secrets | Stored outside rule definitions, referenced by name | rules get exported and shared, and a rule containing a token is a leaked token | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the service. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to run it continuously** · 5 to 10 US dollars a month, or free on hardware you own - Why: Automations that only run while your laptop is open miss events and then fire a burst, which is worse than not running. - Get it: A small virtual server or a machine at home that stays on. This workload is tiny. - Verify: The service runs on a schedule and survives a reboot - [ ] **An HTTPS endpoint for inbound webhooks** · roughly 10 to 15 US dollars a year for a domain - Why: Phase 4 needs one, and most services will not deliver to plain HTTP. - Get it: A domain pointed at your server with automatic certificates, or a tunnel for development. - Verify: A test delivery reaches your endpoint over HTTPS - [ ] **Credentials for two real services** · free for most personal API access - Why: One trigger source and one action target, both real. A test harness will not have the rate limits, pagination and redelivery behaviour that shape this build. - Get it: API keys or tokens for two services you actually use, scoped as narrowly as they allow. - Verify: A rule polls one and acts on the other successfully - [ ] **An understanding of which rules could loop** · free - Why: Phase 2's ceiling exists because a loop with external side effects is the expensive failure, and knowing which of your rules could form one determines how tight the ceiling should be. - Get it: Before enabling any rule, write down whether its action could cause its own trigger, directly or through another rule. - Verify: Each rule has a loop assessment recorded ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. - consumer integration catalog - smart-home integrations - mobile applets - easy UI - maintenance when APIs change If one of those is essential to you, that is the reason to keep paying for IFTTT, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · IFTTT The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me small automations connecting services you use, that do not fire twice and cannot run away to replace IFTTT. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two failure modes define this build and both are worse than the automation is valuable. The first is firing twice for one event, which sends a duplicate message or creates a duplicate record, and it happens whenever a poll overlaps or a webhook is redelivered. The second is a loop: an action that causes a trigger that causes the action. A loop with an external side effect can send hundreds of messages before you notice. So idempotency and a rate ceiling are Phase 2, before any useful automation exists. Both are cheap to build first and expensive to retrofit after an incident. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite · a few dozen rules and a few thousand runs a month - Triggers: Scheduled polls with change detection, plus inbound webhooks · most services offer one or the other and both need different idempotency handling - Idempotency: An event key per trigger, unique in the database · the single mechanism that makes exactly-once possible - Actions: Outbound requests with retries and a per-rule ceiling · a retry that is not idempotent is a second side effect - Secrets: Stored outside rule definitions, referenced by name · rules get exported and shared, and a rule containing a token is a leaked token ### Data model (create this before Phase 1) - rules(id, name, enabled, trigger_kind, trigger_config_json, action_kind, action_config_json, max_runs_per_hour) - events(id, rule_id, event_key, payload_json, observed_at, state) · unique on rule and event key, which is the idempotency - runs(id, event_id, started_at, finished_at, state, attempts, request_summary, response_summary, error) - secrets(name, value_encrypted) · referenced from configs by name, never inlined - cursors(rule_id, value, updated_at) · what a polled trigger has already seen - The unique constraint on rule and event key is what makes exactly-once real rather than intended. ### Phase 1 · One trigger, one action, logged Build: A rule with a scheduled poll and an outbound request action. Log every run with a summary of the request and the response. Redact secret values from the log. Nothing clever yet. Trap: Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. Done when: A rule polls, fires and records a run with request and response summaries. Secret values are absent from the log, verified by grepping for a known token. A failing action records the error rather than disappearing. Disabling a rule stops it firing. Do not build yet: No idempotency, rate limits or webhooks. ### Phase 2 · Exactly once, and a ceiling Build: Derive an event key per trigger and enforce uniqueness in the database. A per-rule limit on runs per hour that disables the rule and notifies you when exceeded. Test both by causing them deliberately. Trap: Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. Trap: A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. Done when: The same event observed twice produces one run, enforced by a database constraint rather than a check. A deliberately looping rule hits its ceiling, disables itself and notifies. Overlapping polls do not double fire, verified by running two concurrently. A disabled-by-ceiling rule requires deliberate re-enabling. Do not build yet: No webhooks or secret management yet. ### Phase 3 · Polling politely with cursors Build: Store a cursor per rule and request only what is new. Use conditional requests where the source supports them. Back off on errors and on rate limit responses. Handle a cursor the source no longer accepts. Trap: Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. Done when: A poll requests only items after the cursor. An expired or rejected cursor triggers a documented recovery rather than an error. Backoff increases on repeated failure and respects a retry hint. A source producing more items than one page returns is paged through rather than truncated. Do not build yet: No webhooks or sharing yet. ### Phase 4 · Inbound webhooks Build: An inbound endpoint per rule with an unguessable path. Verify a signature where the source provides one. Derive the event key from the payload's own identifier so a redelivery deduplicates. Respond quickly and process asynchronously. Trap: Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. Done when: A webhook delivery creates an event and returns quickly, verified by timing the response. Redelivering the same payload produces one run. A payload with a bad signature is rejected. An unguessable path is required and a wrong path returns nothing informative. Do not build yet: No secret management or sharing yet. ### Phase 5 · Secrets outside the rules Build: Store secrets separately, encrypted, referenced from configs by name. Exporting a rule exports the reference rather than the value. Importing prompts for the secrets it needs rather than failing obscurely. Trap: A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. Done when: A rule config contains a secret reference and no value, verified by inspecting the export. Importing a rule lists the secrets it needs and prompts for them. A missing secret produces a clear error naming it rather than an authorisation failure from the target service. Secrets are encrypted at rest. Do not build yet: Operations come last. ### Phase 6 · Running it continuously Build: Run as a service surviving restarts. Alert on a rule that has not fired in its expected window, on repeated failures and on a ceiling trip. Back up the database. Report per-rule run counts and failure rates. Trap: A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here. Done when: The service survives a restart with cursors intact. A rule that stops firing within its expected window produces an alert. Repeated failures alert rather than retrying indefinitely. A restore reproduces rules, cursors and secrets, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - The consumer integration catalogue, which is hundreds of maintained connectors and is the entire product. - Smart home integrations, which need device protocols and local network presence. - Mobile applets and the triggers that come from a phone's sensors and location. - Someone else maintaining a connector when a service changes its API, which now happens to you without warning. ### README must contain - That idempotency is enforced by a database constraint rather than a check, and why - That a per-rule ceiling disables a runaway rather than warning about it - That webhooks acknowledge immediately and process asynchronously - That secrets are referenced by name and never appear in an exported rule - That rules have an expected frequency, because silence is the failure mode ===== AGENTS.md ===== # Agent instructions · IFTTT indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with SQLite, Scheduled polls with change detection, plus inbound webhooks, An event key per trigger, unique in the database, Outbound requests with retries and a per-rule ceiling, Stored outside rule definitions, referenced by name. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. ===== BUILD_PLAN.md ===== # Build plan · IFTTT Rules connecting a trigger to an action: polled sources with change detection or inbound webhooks, an event key that makes each event fire exactly once, a per-rule rate ceiling that stops a runaway, a run log showing what happened, and secrets that live outside the rule definitions. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One trigger, one action, logged A rule fires and you can see exactly what it sent and what came back. ### Steps 1. Model rules with configs Trigger and action as data, so a rule is inspectable rather than code. Files: `src/rules.js` 2. Log runs with summaries Enough to debug, redacted at the boundary. Files: `src/log.js` 3. Redact structurally A logging function that cannot emit a secret value, rather than discipline. Files: `src/redact.js` ### Done when - [ ] A rule fires and records request and response summaries - [ ] No secret value appears in the log - [ ] A failing action records its error - [ ] Disabling stops the rule firing ### Watch out - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. ## Phase 2 · Exactly once, and a ceiling One event fires one action, and a runaway rule stops itself. ### Steps 1. Derive and enforce event keys Unique on rule and key, in the database. Insert failure means duplicate. Files: `src/events.js`, `migrations/` 2. Add a per-rule ceiling that disables Not a warning. A loop with side effects is the case this exists for. Files: `src/limits.js` 3. Cause both deliberately A loop and a concurrent double poll. Untested safety is not safety. Files: `test/idempotency.test.js` ### Done when - [ ] A repeated event produces one run, enforced by constraint - [ ] A looping rule disables itself and notifies - [ ] Concurrent polls do not double fire - [ ] Re-enabling after a ceiling trip is deliberate ### Watch out - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. ## Phase 3 · Polling politely with cursors A polled source is checked without refetching everything and without missing items. ### Steps 1. Store and advance a cursor After a successful batch, not before. A cursor advanced on failure loses events. Files: `src/poll.js` 2. Use conditional requests where available Cheaper for you and politer to the source. Files: `src/http.js` 3. Page rather than truncate If a poll returns a full page, there may be more. Truncating loses events silently. Files: `src/poll.js` ### Done when - [ ] A poll requests only items after the cursor - [ ] A rejected cursor triggers documented recovery - [ ] Backoff increases and respects a retry hint - [ ] A full page is paged through rather than truncated ### Watch out - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. ## Phase 4 · Inbound webhooks A service can push to you, and a replayed delivery does not fire twice. ### Steps 1. Acknowledge fast, process later Queue and return. Slow processing inside the request causes redelivery. Files: `src/webhook.js` 2. Verify signatures where available And where not, rely on the unguessable path and say so in the documentation. Files: `src/webhook.js` 3. Key on the payload's own identifier So a redelivery deduplicates naturally. Files: `src/events.js` ### Done when - [ ] A delivery returns quickly and is processed asynchronously - [ ] A redelivery produces one run - [ ] A bad signature is rejected - [ ] A wrong path returns nothing informative ### Watch out - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. ## Phase 5 · Secrets outside the rules A rule can be exported and shared without leaking a token. ### Steps 1. Reference secrets by name Configs never contain values. Grep an export to confirm. Files: `src/secrets.js` 2. Prompt on import List what is needed. A rule that fails with a service authorisation error is hard to diagnose. Files: `src/import.js` 3. Encrypt at rest With the key outside the database. Losing it loses the secrets, which is the right failure. Files: `src/crypto.js` ### Done when - [ ] An exported rule contains no secret value - [ ] Import prompts for required secrets - [ ] A missing secret produces a clear named error - [ ] Secrets are encrypted at rest ### Watch out - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. ## Not in this build - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. ## After v1, if you want it - A filter step between trigger and action, which removes most of the need for more rules - A dry run mode showing what a rule would do without doing it - Templating action payloads from trigger data, which is where rules become genuinely useful ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, events, runs and cursors. DB_PATH=./data/rules.db # Required · secret. Encrypts the secret store. Kept outside the database; losing it loses the secrets deliberately. SECRETS_KEY=generate-a-long-random-value # Required. Default ceiling per rule. Exceeding it disables the rule rather than warning. DEFAULT_MAX_RUNS_PER_HOUR=30 # Required. Where inbound endpoints live. Must be HTTPS. WEBHOOK_BASE_URL=https://hooks.example.com # Required. Floor on polling frequency. Politeness to the sources and a limit on your own request volume. POLL_MIN_INTERVAL_SECONDS=300 # Required · secret. Where ceiling trips, failures and silent rules are reported. ALERT_WEBHOOK=https://chat.example.com/hook
You are building a production product version of IFTTT. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # IFTTT · product brief ## Problem For a few personal applets, a script or simple webhook worker is enough; the full consumer integration catalog and smart-home breadth are what you lose. ## Product outcome Automations that fire exactly once, stop themselves if they start looping, keep secrets out of anything you might share, and tell you when a rule has gone quiet. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - API keys/OAuth - webhook endpoint or scheduler - small hosted function - secrets storage ## Explicit non-goals for v1 - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. - consumer integration catalog - smart-home integrations - mobile applets - easy UI - maintenance when APIs change ## Success criteria - A repeated event is confirmed to produce one run via database constraint - A deliberately looping rule is confirmed to disable itself - No secret value appears in a log or an exported rule - A webhook redelivery is confirmed to produce one run - A silent rule is confirmed to produce an alert ===== BRIEF.md ===== # Build brief · IFTTT The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me small automations connecting services you use, that do not fire twice and cannot run away to replace IFTTT. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two failure modes define this build and both are worse than the automation is valuable. The first is firing twice for one event, which sends a duplicate message or creates a duplicate record, and it happens whenever a poll overlaps or a webhook is redelivered. The second is a loop: an action that causes a trigger that causes the action. A loop with an external side effect can send hundreds of messages before you notice. So idempotency and a rate ceiling are Phase 2, before any useful automation exists. Both are cheap to build first and expensive to retrofit after an incident. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite · a few dozen rules and a few thousand runs a month - Triggers: Scheduled polls with change detection, plus inbound webhooks · most services offer one or the other and both need different idempotency handling - Idempotency: An event key per trigger, unique in the database · the single mechanism that makes exactly-once possible - Actions: Outbound requests with retries and a per-rule ceiling · a retry that is not idempotent is a second side effect - Secrets: Stored outside rule definitions, referenced by name · rules get exported and shared, and a rule containing a token is a leaked token ### Data model (create this before Phase 1) - rules(id, name, enabled, trigger_kind, trigger_config_json, action_kind, action_config_json, max_runs_per_hour) - events(id, rule_id, event_key, payload_json, observed_at, state) · unique on rule and event key, which is the idempotency - runs(id, event_id, started_at, finished_at, state, attempts, request_summary, response_summary, error) - secrets(name, value_encrypted) · referenced from configs by name, never inlined - cursors(rule_id, value, updated_at) · what a polled trigger has already seen - The unique constraint on rule and event key is what makes exactly-once real rather than intended. ### Phase 1 · One trigger, one action, logged Build: A rule with a scheduled poll and an outbound request action. Log every run with a summary of the request and the response. Redact secret values from the log. Nothing clever yet. Trap: Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. Done when: A rule polls, fires and records a run with request and response summaries. Secret values are absent from the log, verified by grepping for a known token. A failing action records the error rather than disappearing. Disabling a rule stops it firing. Do not build yet: No idempotency, rate limits or webhooks. ### Phase 2 · Exactly once, and a ceiling Build: Derive an event key per trigger and enforce uniqueness in the database. A per-rule limit on runs per hour that disables the rule and notifies you when exceeded. Test both by causing them deliberately. Trap: Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. Trap: A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. Done when: The same event observed twice produces one run, enforced by a database constraint rather than a check. A deliberately looping rule hits its ceiling, disables itself and notifies. Overlapping polls do not double fire, verified by running two concurrently. A disabled-by-ceiling rule requires deliberate re-enabling. Do not build yet: No webhooks or secret management yet. ### Phase 3 · Polling politely with cursors Build: Store a cursor per rule and request only what is new. Use conditional requests where the source supports them. Back off on errors and on rate limit responses. Handle a cursor the source no longer accepts. Trap: Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. Done when: A poll requests only items after the cursor. An expired or rejected cursor triggers a documented recovery rather than an error. Backoff increases on repeated failure and respects a retry hint. A source producing more items than one page returns is paged through rather than truncated. Do not build yet: No webhooks or sharing yet. ### Phase 4 · Inbound webhooks Build: An inbound endpoint per rule with an unguessable path. Verify a signature where the source provides one. Derive the event key from the payload's own identifier so a redelivery deduplicates. Respond quickly and process asynchronously. Trap: Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. Done when: A webhook delivery creates an event and returns quickly, verified by timing the response. Redelivering the same payload produces one run. A payload with a bad signature is rejected. An unguessable path is required and a wrong path returns nothing informative. Do not build yet: No secret management or sharing yet. ### Phase 5 · Secrets outside the rules Build: Store secrets separately, encrypted, referenced from configs by name. Exporting a rule exports the reference rather than the value. Importing prompts for the secrets it needs rather than failing obscurely. Trap: A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. Done when: A rule config contains a secret reference and no value, verified by inspecting the export. Importing a rule lists the secrets it needs and prompts for them. A missing secret produces a clear error naming it rather than an authorisation failure from the target service. Secrets are encrypted at rest. Do not build yet: Operations come last. ### Phase 6 · Running it continuously Build: Run as a service surviving restarts. Alert on a rule that has not fired in its expected window, on repeated failures and on a ceiling trip. Back up the database. Report per-rule run counts and failure rates. Trap: A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here. Done when: The service survives a restart with cursors intact. A rule that stops firing within its expected window produces an alert. Repeated failures alert rather than retrying indefinitely. A restore reproduces rules, cursors and secrets, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - The consumer integration catalogue, which is hundreds of maintained connectors and is the entire product. - Smart home integrations, which need device protocols and local network presence. - Mobile applets and the triggers that come from a phone's sensors and location. - Someone else maintaining a connector when a service changes its API, which now happens to you without warning. ### README must contain - That idempotency is enforced by a database constraint rather than a check, and why - That a per-rule ceiling disables a runaway rather than warning about it - That webhooks acknowledge immediately and process asynchronously - That secrets are referenced by name and never appear in an exported rule - That rules have an expected frequency, because silence is the failure mode ===== ARCHITECTURE.md ===== # Architecture · IFTTT ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite | a few dozen rules and a few thousand runs a month | | Triggers | Scheduled polls with change detection, plus inbound webhooks | most services offer one or the other and both need different idempotency handling | | Idempotency | An event key per trigger, unique in the database | the single mechanism that makes exactly-once possible | | Actions | Outbound requests with retries and a per-rule ceiling | a retry that is not idempotent is a second side effect | | Secrets | Stored outside rule definitions, referenced by name | rules get exported and shared, and a rule containing a token is a leaked token | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Rules | Trigger and action configuration as data. | Any storage. Configs referencing secrets by name is the property to keep. | | Events | Exactly-once through a database constraint. | Not swappable for an application-level check, which races under concurrency. | | Limits | Stopping a runaway. | Any implementation that disables rather than warns. A warning does not stop a loop. | | Triggers | Polling with cursors and inbound webhooks. | Sources differ; the cursor discipline and fast webhook acknowledgement do not. | | Secrets | Encrypted storage outside rule definitions. | A managed secret manager is a straight upgrade. Inline values are the thing being prevented. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Rules, events, runs and cursors. - `SECRETS_KEY` · required, secret · Encrypts the secret store. Kept outside the database; losing it loses the secrets deliberately. - `DEFAULT_MAX_RUNS_PER_HOUR` · required · Default ceiling per rule. Exceeding it disables the rule rather than warning. - `WEBHOOK_BASE_URL` · required · Where inbound endpoints live. Must be HTTPS. - `POLL_MIN_INTERVAL_SECONDS` · required · Floor on polling frequency. Politeness to the sources and a limit on your own request volume. - `ALERT_WEBHOOK` · required, secret · Where ceiling trips, failures and silent rules are reported. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · IFTTT product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with SQLite, Scheduled polls with change detection, plus inbound webhooks, An event key per trigger, unique in the database, Outbound requests with retries and a per-rule ceiling, Stored outside rule definitions, referenced by name. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. - A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here. ===== MILESTONES.md ===== # Delivery milestones · IFTTT Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One trigger, one action, logged A rule fires and you can see exactly what it sent and what came back. ### Steps 1. Model rules with configs Trigger and action as data, so a rule is inspectable rather than code. Files: `src/rules.js` 2. Log runs with summaries Enough to debug, redacted at the boundary. Files: `src/log.js` 3. Redact structurally A logging function that cannot emit a secret value, rather than discipline. Files: `src/redact.js` ### Done when - [ ] A rule fires and records request and response summaries - [ ] No secret value appears in the log - [ ] A failing action records its error - [ ] Disabling stops the rule firing ### Watch out - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. ## M2 · Exactly once, and a ceiling One event fires one action, and a runaway rule stops itself. ### Steps 1. Derive and enforce event keys Unique on rule and key, in the database. Insert failure means duplicate. Files: `src/events.js`, `migrations/` 2. Add a per-rule ceiling that disables Not a warning. A loop with side effects is the case this exists for. Files: `src/limits.js` 3. Cause both deliberately A loop and a concurrent double poll. Untested safety is not safety. Files: `test/idempotency.test.js` ### Done when - [ ] A repeated event produces one run, enforced by constraint - [ ] A looping rule disables itself and notifies - [ ] Concurrent polls do not double fire - [ ] Re-enabling after a ceiling trip is deliberate ### Watch out - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. ## M3 · Polling politely with cursors A polled source is checked without refetching everything and without missing items. ### Steps 1. Store and advance a cursor After a successful batch, not before. A cursor advanced on failure loses events. Files: `src/poll.js` 2. Use conditional requests where available Cheaper for you and politer to the source. Files: `src/http.js` 3. Page rather than truncate If a poll returns a full page, there may be more. Truncating loses events silently. Files: `src/poll.js` ### Done when - [ ] A poll requests only items after the cursor - [ ] A rejected cursor triggers documented recovery - [ ] Backoff increases and respects a retry hint - [ ] A full page is paged through rather than truncated ### Watch out - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. ## M4 · Inbound webhooks A service can push to you, and a replayed delivery does not fire twice. ### Steps 1. Acknowledge fast, process later Queue and return. Slow processing inside the request causes redelivery. Files: `src/webhook.js` 2. Verify signatures where available And where not, rely on the unguessable path and say so in the documentation. Files: `src/webhook.js` 3. Key on the payload's own identifier So a redelivery deduplicates naturally. Files: `src/events.js` ### Done when - [ ] A delivery returns quickly and is processed asynchronously - [ ] A redelivery produces one run - [ ] A bad signature is rejected - [ ] A wrong path returns nothing informative ### Watch out - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. ## M5 · Secrets outside the rules A rule can be exported and shared without leaking a token. ### Steps 1. Reference secrets by name Configs never contain values. Grep an export to confirm. Files: `src/secrets.js` 2. Prompt on import List what is needed. A rule that fails with a service authorisation error is hard to diagnose. Files: `src/import.js` 3. Encrypt at rest With the key outside the database. Losing it loses the secrets, which is the right failure. Files: `src/crypto.js` ### Done when - [ ] An exported rule contains no secret value - [ ] Import prompts for required secrets - [ ] A missing secret produces a clear named error - [ ] Secrets are encrypted at rest ### Watch out - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. ## M6 · Running it continuously (production only) Rules run without supervision and you find out when one stops. ### Steps 1. Run as a service with durable cursors Restart safe. A cursor lost on restart replays or skips, and both are bad. Files: `src/service.js` 2. Alert on silence and on failure Expected frequency per rule. Silence is the failure you will otherwise miss. Files: `src/health.js` 3. Back up and restore Rules, cursors and secrets. Without the key the secrets are gone, which is deliberate. Files: `scripts/backup.sh`, `README.md` ### Done when - [ ] The service survives a restart with cursors intact - [ ] A silent rule produces an alert - [ ] Repeated failures alert rather than retrying forever - [ ] A restore reproduces rules, cursors and secrets ### Watch out - A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here. ===== OPERATIONS.md ===== # Operations · IFTTT ## Backup Rules, cursors and the encrypted secret store, with the encryption key kept separately. A restore without cursors will replay or skip events depending on the trigger, so they are part of the data rather than a cache. ## Restore Restore all three, start the service and confirm a rule fires once. Cursors being correct is what makes that a clean restore rather than a burst of replays. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Per-rule run counts against expected frequency, failure rates, and ceiling trips. Silence is the failure mode that needs active detection, which is why expected frequency is configured per rule. ## Incident checklist A loop is the expensive incident and the ceiling is the control; if a loop ran past it, the ceiling was warning rather than disabling. A duplicate side effect means the event key was not derived from something stable, which is worth fixing at the trigger rather than downstream. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A repeated event is confirmed to produce one run via database constraint - [ ] A deliberately looping rule is confirmed to disable itself - [ ] No secret value appears in a log or an exported rule - [ ] A webhook redelivery is confirmed to produce one run - [ ] A silent rule is confirmed to produce an alert ## Launch constraint Do not market omitted IFTTT capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, events, runs and cursors. DB_PATH=./data/rules.db # Required · secret. Encrypts the secret store. Kept outside the database; losing it loses the secrets deliberately. SECRETS_KEY=generate-a-long-random-value # Required. Default ceiling per rule. Exceeding it disables the rule rather than warning. DEFAULT_MAX_RUNS_PER_HOUR=30 # Required. Where inbound endpoints live. Must be HTTPS. WEBHOOK_BASE_URL=https://hooks.example.com # Required. Floor on polling frequency. Politeness to the sources and a limit on your own request volume. POLL_MIN_INTERVAL_SECONDS=300 # Required · secret. Where ceiling trips, failures and silent rules are reported. ALERT_WEBHOOK=https://chat.example.com/hook
# IFTTT · indie build Rules connecting a trigger to an action: polled sources with change detection or inbound webhooks, an event key that makes each event fire exactly once, a per-rule rate ceiling that stops a runaway, a run log showing what happened, and secrets that live outside the rule definitions. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite | a few dozen rules and a few thousand runs a month | | Triggers | Scheduled polls with change detection, plus inbound webhooks | most services offer one or the other and both need different idempotency handling | | Idempotency | An event key per trigger, unique in the database | the single mechanism that makes exactly-once possible | | Actions | Outbound requests with retries and a per-rule ceiling | a retry that is not idempotent is a second side effect | | Secrets | Stored outside rule definitions, referenced by name | rules get exported and shared, and a rule containing a token is a leaked token | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the service. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to run it continuously** · 5 to 10 US dollars a month, or free on hardware you own - Why: Automations that only run while your laptop is open miss events and then fire a burst, which is worse than not running. - Get it: A small virtual server or a machine at home that stays on. This workload is tiny. - Verify: The service runs on a schedule and survives a reboot - [ ] **An HTTPS endpoint for inbound webhooks** · roughly 10 to 15 US dollars a year for a domain - Why: Phase 4 needs one, and most services will not deliver to plain HTTP. - Get it: A domain pointed at your server with automatic certificates, or a tunnel for development. - Verify: A test delivery reaches your endpoint over HTTPS - [ ] **Credentials for two real services** · free for most personal API access - Why: One trigger source and one action target, both real. A test harness will not have the rate limits, pagination and redelivery behaviour that shape this build. - Get it: API keys or tokens for two services you actually use, scoped as narrowly as they allow. - Verify: A rule polls one and acts on the other successfully - [ ] **An understanding of which rules could loop** · free - Why: Phase 2's ceiling exists because a loop with external side effects is the expensive failure, and knowing which of your rules could form one determines how tight the ceiling should be. - Get it: Before enabling any rule, write down whether its action could cause its own trigger, directly or through another rule. - Verify: Each rule has a loop assessment recorded ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. - consumer integration catalog - smart-home integrations - mobile applets - easy UI - maintenance when APIs change If one of those is essential to you, that is the reason to keep paying for IFTTT, and the README should say so rather than pretend.
# Build brief · IFTTT The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me small automations connecting services you use, that do not fire twice and cannot run away to replace IFTTT. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two failure modes define this build and both are worse than the automation is valuable. The first is firing twice for one event, which sends a duplicate message or creates a duplicate record, and it happens whenever a poll overlaps or a webhook is redelivered. The second is a loop: an action that causes a trigger that causes the action. A loop with an external side effect can send hundreds of messages before you notice. So idempotency and a rate ceiling are Phase 2, before any useful automation exists. Both are cheap to build first and expensive to retrofit after an incident. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite · a few dozen rules and a few thousand runs a month - Triggers: Scheduled polls with change detection, plus inbound webhooks · most services offer one or the other and both need different idempotency handling - Idempotency: An event key per trigger, unique in the database · the single mechanism that makes exactly-once possible - Actions: Outbound requests with retries and a per-rule ceiling · a retry that is not idempotent is a second side effect - Secrets: Stored outside rule definitions, referenced by name · rules get exported and shared, and a rule containing a token is a leaked token ### Data model (create this before Phase 1) - rules(id, name, enabled, trigger_kind, trigger_config_json, action_kind, action_config_json, max_runs_per_hour) - events(id, rule_id, event_key, payload_json, observed_at, state) · unique on rule and event key, which is the idempotency - runs(id, event_id, started_at, finished_at, state, attempts, request_summary, response_summary, error) - secrets(name, value_encrypted) · referenced from configs by name, never inlined - cursors(rule_id, value, updated_at) · what a polled trigger has already seen - The unique constraint on rule and event key is what makes exactly-once real rather than intended. ### Phase 1 · One trigger, one action, logged Build: A rule with a scheduled poll and an outbound request action. Log every run with a summary of the request and the response. Redact secret values from the log. Nothing clever yet. Trap: Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. Done when: A rule polls, fires and records a run with request and response summaries. Secret values are absent from the log, verified by grepping for a known token. A failing action records the error rather than disappearing. Disabling a rule stops it firing. Do not build yet: No idempotency, rate limits or webhooks. ### Phase 2 · Exactly once, and a ceiling Build: Derive an event key per trigger and enforce uniqueness in the database. A per-rule limit on runs per hour that disables the rule and notifies you when exceeded. Test both by causing them deliberately. Trap: Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. Trap: A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. Done when: The same event observed twice produces one run, enforced by a database constraint rather than a check. A deliberately looping rule hits its ceiling, disables itself and notifies. Overlapping polls do not double fire, verified by running two concurrently. A disabled-by-ceiling rule requires deliberate re-enabling. Do not build yet: No webhooks or secret management yet. ### Phase 3 · Polling politely with cursors Build: Store a cursor per rule and request only what is new. Use conditional requests where the source supports them. Back off on errors and on rate limit responses. Handle a cursor the source no longer accepts. Trap: Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. Done when: A poll requests only items after the cursor. An expired or rejected cursor triggers a documented recovery rather than an error. Backoff increases on repeated failure and respects a retry hint. A source producing more items than one page returns is paged through rather than truncated. Do not build yet: No webhooks or sharing yet. ### Phase 4 · Inbound webhooks Build: An inbound endpoint per rule with an unguessable path. Verify a signature where the source provides one. Derive the event key from the payload's own identifier so a redelivery deduplicates. Respond quickly and process asynchronously. Trap: Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. Done when: A webhook delivery creates an event and returns quickly, verified by timing the response. Redelivering the same payload produces one run. A payload with a bad signature is rejected. An unguessable path is required and a wrong path returns nothing informative. Do not build yet: No secret management or sharing yet. ### Phase 5 · Secrets outside the rules Build: Store secrets separately, encrypted, referenced from configs by name. Exporting a rule exports the reference rather than the value. Importing prompts for the secrets it needs rather than failing obscurely. Trap: A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. Done when: A rule config contains a secret reference and no value, verified by inspecting the export. Importing a rule lists the secrets it needs and prompts for them. A missing secret produces a clear error naming it rather than an authorisation failure from the target service. Secrets are encrypted at rest. Do not build yet: Operations come last. ### Phase 6 · Running it continuously Build: Run as a service surviving restarts. Alert on a rule that has not fired in its expected window, on repeated failures and on a ceiling trip. Back up the database. Report per-rule run counts and failure rates. Trap: A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here. Done when: The service survives a restart with cursors intact. A rule that stops firing within its expected window produces an alert. Repeated failures alert rather than retrying indefinitely. A restore reproduces rules, cursors and secrets, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - The consumer integration catalogue, which is hundreds of maintained connectors and is the entire product. - Smart home integrations, which need device protocols and local network presence. - Mobile applets and the triggers that come from a phone's sensors and location. - Someone else maintaining a connector when a service changes its API, which now happens to you without warning. ### README must contain - That idempotency is enforced by a database constraint rather than a check, and why - That a per-rule ceiling disables a runaway rather than warning about it - That webhooks acknowledge immediately and process asynchronously - That secrets are referenced by name and never appear in an exported rule - That rules have an expected frequency, because silence is the failure mode
# Agent instructions · IFTTT indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with SQLite, Scheduled polls with change detection, plus inbound webhooks, An event key per trigger, unique in the database, Outbound requests with retries and a per-rule ceiling, Stored outside rule definitions, referenced by name. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported.
# Build plan · IFTTT Rules connecting a trigger to an action: polled sources with change detection or inbound webhooks, an event key that makes each event fire exactly once, a per-rule rate ceiling that stops a runaway, a run log showing what happened, and secrets that live outside the rule definitions. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One trigger, one action, logged A rule fires and you can see exactly what it sent and what came back. ### Steps 1. Model rules with configs Trigger and action as data, so a rule is inspectable rather than code. Files: `src/rules.js` 2. Log runs with summaries Enough to debug, redacted at the boundary. Files: `src/log.js` 3. Redact structurally A logging function that cannot emit a secret value, rather than discipline. Files: `src/redact.js` ### Done when - [ ] A rule fires and records request and response summaries - [ ] No secret value appears in the log - [ ] A failing action records its error - [ ] Disabling stops the rule firing ### Watch out - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. ## Phase 2 · Exactly once, and a ceiling One event fires one action, and a runaway rule stops itself. ### Steps 1. Derive and enforce event keys Unique on rule and key, in the database. Insert failure means duplicate. Files: `src/events.js`, `migrations/` 2. Add a per-rule ceiling that disables Not a warning. A loop with side effects is the case this exists for. Files: `src/limits.js` 3. Cause both deliberately A loop and a concurrent double poll. Untested safety is not safety. Files: `test/idempotency.test.js` ### Done when - [ ] A repeated event produces one run, enforced by constraint - [ ] A looping rule disables itself and notifies - [ ] Concurrent polls do not double fire - [ ] Re-enabling after a ceiling trip is deliberate ### Watch out - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. ## Phase 3 · Polling politely with cursors A polled source is checked without refetching everything and without missing items. ### Steps 1. Store and advance a cursor After a successful batch, not before. A cursor advanced on failure loses events. Files: `src/poll.js` 2. Use conditional requests where available Cheaper for you and politer to the source. Files: `src/http.js` 3. Page rather than truncate If a poll returns a full page, there may be more. Truncating loses events silently. Files: `src/poll.js` ### Done when - [ ] A poll requests only items after the cursor - [ ] A rejected cursor triggers documented recovery - [ ] Backoff increases and respects a retry hint - [ ] A full page is paged through rather than truncated ### Watch out - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. ## Phase 4 · Inbound webhooks A service can push to you, and a replayed delivery does not fire twice. ### Steps 1. Acknowledge fast, process later Queue and return. Slow processing inside the request causes redelivery. Files: `src/webhook.js` 2. Verify signatures where available And where not, rely on the unguessable path and say so in the documentation. Files: `src/webhook.js` 3. Key on the payload's own identifier So a redelivery deduplicates naturally. Files: `src/events.js` ### Done when - [ ] A delivery returns quickly and is processed asynchronously - [ ] A redelivery produces one run - [ ] A bad signature is rejected - [ ] A wrong path returns nothing informative ### Watch out - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. ## Phase 5 · Secrets outside the rules A rule can be exported and shared without leaking a token. ### Steps 1. Reference secrets by name Configs never contain values. Grep an export to confirm. Files: `src/secrets.js` 2. Prompt on import List what is needed. A rule that fails with a service authorisation error is hard to diagnose. Files: `src/import.js` 3. Encrypt at rest With the key outside the database. Losing it loses the secrets, which is the right failure. Files: `src/crypto.js` ### Done when - [ ] An exported rule contains no secret value - [ ] Import prompts for required secrets - [ ] A missing secret produces a clear named error - [ ] Secrets are encrypted at rest ### Watch out - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. ## Not in this build - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. ## After v1, if you want it - A filter step between trigger and action, which removes most of the need for more rules - A dry run mode showing what a rule would do without doing it - Templating action payloads from trigger data, which is where rules become genuinely useful
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, events, runs and cursors. DB_PATH=./data/rules.db # Required · secret. Encrypts the secret store. Kept outside the database; losing it loses the secrets deliberately. SECRETS_KEY=generate-a-long-random-value # Required. Default ceiling per rule. Exceeding it disables the rule rather than warning. DEFAULT_MAX_RUNS_PER_HOUR=30 # Required. Where inbound endpoints live. Must be HTTPS. WEBHOOK_BASE_URL=https://hooks.example.com # Required. Floor on polling frequency. Politeness to the sources and a limit on your own request volume. POLL_MIN_INTERVAL_SECONDS=300 # Required · secret. Where ceiling trips, failures and silent rules are reported. ALERT_WEBHOOK=https://chat.example.com/hook
# IFTTT · product brief ## Problem For a few personal applets, a script or simple webhook worker is enough; the full consumer integration catalog and smart-home breadth are what you lose. ## Product outcome Automations that fire exactly once, stop themselves if they start looping, keep secrets out of anything you might share, and tell you when a rule has gone quiet. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - API keys/OAuth - webhook endpoint or scheduler - small hosted function - secrets storage ## Explicit non-goals for v1 - The consumer integration catalogue. - Smart home integrations. - Mobile triggers from sensors and location. - Anyone maintaining a connector when a service changes. - consumer integration catalog - smart-home integrations - mobile applets - easy UI - maintenance when APIs change ## Success criteria - A repeated event is confirmed to produce one run via database constraint - A deliberately looping rule is confirmed to disable itself - No secret value appears in a log or an exported rule - A webhook redelivery is confirmed to produce one run - A silent rule is confirmed to produce an alert
# Build brief · IFTTT The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me small automations connecting services you use, that do not fire twice and cannot run away to replace IFTTT. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two failure modes define this build and both are worse than the automation is valuable. The first is firing twice for one event, which sends a duplicate message or creates a duplicate record, and it happens whenever a poll overlaps or a webhook is redelivered. The second is a loop: an action that causes a trigger that causes the action. A loop with an external side effect can send hundreds of messages before you notice. So idempotency and a rate ceiling are Phase 2, before any useful automation exists. Both are cheap to build first and expensive to retrofit after an incident. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite · a few dozen rules and a few thousand runs a month - Triggers: Scheduled polls with change detection, plus inbound webhooks · most services offer one or the other and both need different idempotency handling - Idempotency: An event key per trigger, unique in the database · the single mechanism that makes exactly-once possible - Actions: Outbound requests with retries and a per-rule ceiling · a retry that is not idempotent is a second side effect - Secrets: Stored outside rule definitions, referenced by name · rules get exported and shared, and a rule containing a token is a leaked token ### Data model (create this before Phase 1) - rules(id, name, enabled, trigger_kind, trigger_config_json, action_kind, action_config_json, max_runs_per_hour) - events(id, rule_id, event_key, payload_json, observed_at, state) · unique on rule and event key, which is the idempotency - runs(id, event_id, started_at, finished_at, state, attempts, request_summary, response_summary, error) - secrets(name, value_encrypted) · referenced from configs by name, never inlined - cursors(rule_id, value, updated_at) · what a polled trigger has already seen - The unique constraint on rule and event key is what makes exactly-once real rather than intended. ### Phase 1 · One trigger, one action, logged Build: A rule with a scheduled poll and an outbound request action. Log every run with a summary of the request and the response. Redact secret values from the log. Nothing clever yet. Trap: Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. Done when: A rule polls, fires and records a run with request and response summaries. Secret values are absent from the log, verified by grepping for a known token. A failing action records the error rather than disappearing. Disabling a rule stops it firing. Do not build yet: No idempotency, rate limits or webhooks. ### Phase 2 · Exactly once, and a ceiling Build: Derive an event key per trigger and enforce uniqueness in the database. A per-rule limit on runs per hour that disables the rule and notifies you when exceeded. Test both by causing them deliberately. Trap: Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. Trap: A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. Done when: The same event observed twice produces one run, enforced by a database constraint rather than a check. A deliberately looping rule hits its ceiling, disables itself and notifies. Overlapping polls do not double fire, verified by running two concurrently. A disabled-by-ceiling rule requires deliberate re-enabling. Do not build yet: No webhooks or secret management yet. ### Phase 3 · Polling politely with cursors Build: Store a cursor per rule and request only what is new. Use conditional requests where the source supports them. Back off on errors and on rate limit responses. Handle a cursor the source no longer accepts. Trap: Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. Done when: A poll requests only items after the cursor. An expired or rejected cursor triggers a documented recovery rather than an error. Backoff increases on repeated failure and respects a retry hint. A source producing more items than one page returns is paged through rather than truncated. Do not build yet: No webhooks or sharing yet. ### Phase 4 · Inbound webhooks Build: An inbound endpoint per rule with an unguessable path. Verify a signature where the source provides one. Derive the event key from the payload's own identifier so a redelivery deduplicates. Respond quickly and process asynchronously. Trap: Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. Done when: A webhook delivery creates an event and returns quickly, verified by timing the response. Redelivering the same payload produces one run. A payload with a bad signature is rejected. An unguessable path is required and a wrong path returns nothing informative. Do not build yet: No secret management or sharing yet. ### Phase 5 · Secrets outside the rules Build: Store secrets separately, encrypted, referenced from configs by name. Exporting a rule exports the reference rather than the value. Importing prompts for the secrets it needs rather than failing obscurely. Trap: A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. Done when: A rule config contains a secret reference and no value, verified by inspecting the export. Importing a rule lists the secrets it needs and prompts for them. A missing secret produces a clear error naming it rather than an authorisation failure from the target service. Secrets are encrypted at rest. Do not build yet: Operations come last. ### Phase 6 · Running it continuously Build: Run as a service surviving restarts. Alert on a rule that has not fired in its expected window, on repeated failures and on a ceiling trip. Back up the database. Report per-rule run counts and failure rates. Trap: A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here. Done when: The service survives a restart with cursors intact. A rule that stops firing within its expected window produces an alert. Repeated failures alert rather than retrying indefinitely. A restore reproduces rules, cursors and secrets, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - The consumer integration catalogue, which is hundreds of maintained connectors and is the entire product. - Smart home integrations, which need device protocols and local network presence. - Mobile applets and the triggers that come from a phone's sensors and location. - Someone else maintaining a connector when a service changes its API, which now happens to you without warning. ### README must contain - That idempotency is enforced by a database constraint rather than a check, and why - That a per-rule ceiling disables a runaway rather than warning about it - That webhooks acknowledge immediately and process asynchronously - That secrets are referenced by name and never appear in an exported rule - That rules have an expected frequency, because silence is the failure mode
# Architecture · IFTTT ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite | a few dozen rules and a few thousand runs a month | | Triggers | Scheduled polls with change detection, plus inbound webhooks | most services offer one or the other and both need different idempotency handling | | Idempotency | An event key per trigger, unique in the database | the single mechanism that makes exactly-once possible | | Actions | Outbound requests with retries and a per-rule ceiling | a retry that is not idempotent is a second side effect | | Secrets | Stored outside rule definitions, referenced by name | rules get exported and shared, and a rule containing a token is a leaked token | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Rules | Trigger and action configuration as data. | Any storage. Configs referencing secrets by name is the property to keep. | | Events | Exactly-once through a database constraint. | Not swappable for an application-level check, which races under concurrency. | | Limits | Stopping a runaway. | Any implementation that disables rather than warns. A warning does not stop a loop. | | Triggers | Polling with cursors and inbound webhooks. | Sources differ; the cursor discipline and fast webhook acknowledgement do not. | | Secrets | Encrypted storage outside rule definitions. | A managed secret manager is a straight upgrade. Inline values are the thing being prevented. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Rules, events, runs and cursors. - `SECRETS_KEY` · required, secret · Encrypts the secret store. Kept outside the database; losing it loses the secrets deliberately. - `DEFAULT_MAX_RUNS_PER_HOUR` · required · Default ceiling per rule. Exceeding it disables the rule rather than warning. - `WEBHOOK_BASE_URL` · required · Where inbound endpoints live. Must be HTTPS. - `POLL_MIN_INTERVAL_SECONDS` · required · Floor on polling frequency. Politeness to the sources and a limit on your own request volume. - `ALERT_WEBHOOK` · required, secret · Where ceiling trips, failures and silent rules are reported. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · IFTTT product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with SQLite, Scheduled polls with change detection, plus inbound webhooks, An event key per trigger, unique in the database, Outbound requests with retries and a per-rule ceiling, Stored outside rule definitions, referenced by name. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. - A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here.
# Delivery milestones · IFTTT Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One trigger, one action, logged A rule fires and you can see exactly what it sent and what came back. ### Steps 1. Model rules with configs Trigger and action as data, so a rule is inspectable rather than code. Files: `src/rules.js` 2. Log runs with summaries Enough to debug, redacted at the boundary. Files: `src/log.js` 3. Redact structurally A logging function that cannot emit a secret value, rather than discipline. Files: `src/redact.js` ### Done when - [ ] A rule fires and records request and response summaries - [ ] No secret value appears in the log - [ ] A failing action records its error - [ ] Disabling stops the rule firing ### Watch out - Logging the full request including headers writes your tokens into a file you will later share while debugging. Redact secret values at the logging boundary rather than remembering not to log them, since the one you forget is the one that leaks. ## M2 · Exactly once, and a ceiling One event fires one action, and a runaway rule stops itself. ### Steps 1. Derive and enforce event keys Unique on rule and key, in the database. Insert failure means duplicate. Files: `src/events.js`, `migrations/` 2. Add a per-rule ceiling that disables Not a warning. A loop with side effects is the case this exists for. Files: `src/limits.js` 3. Cause both deliberately A loop and a concurrent double poll. Untested safety is not safety. Files: `test/idempotency.test.js` ### Done when - [ ] A repeated event produces one run, enforced by constraint - [ ] A looping rule disables itself and notifies - [ ] Concurrent polls do not double fire - [ ] Re-enabling after a ceiling trip is deliberate ### Watch out - Checking for a duplicate in application code before inserting races with a concurrent poll, and two overlapping runs both pass the check. Put the constraint in the database and treat the insert failure as the duplicate signal. - A rate ceiling that only logs will not stop a loop. It must disable the rule, because a loop producing external side effects is exactly the case where you are not watching. ## M3 · Polling politely with cursors A polled source is checked without refetching everything and without missing items. ### Steps 1. Store and advance a cursor After a successful batch, not before. A cursor advanced on failure loses events. Files: `src/poll.js` 2. Use conditional requests where available Cheaper for you and politer to the source. Files: `src/http.js` 3. Page rather than truncate If a poll returns a full page, there may be more. Truncating loses events silently. Files: `src/poll.js` ### Done when - [ ] A poll requests only items after the cursor - [ ] A rejected cursor triggers documented recovery - [ ] Backoff increases and respects a retry hint - [ ] A full page is paged through rather than truncated ### Watch out - Polling by fetching the latest twenty items and comparing against what you have seen works until more than twenty things happen between polls, and then you silently miss events. Use a cursor the source understands where one exists, and where it does not, poll frequently enough that the window cannot overflow and say so. ## M4 · Inbound webhooks A service can push to you, and a replayed delivery does not fire twice. ### Steps 1. Acknowledge fast, process later Queue and return. Slow processing inside the request causes redelivery. Files: `src/webhook.js` 2. Verify signatures where available And where not, rely on the unguessable path and say so in the documentation. Files: `src/webhook.js` 3. Key on the payload's own identifier So a redelivery deduplicates naturally. Files: `src/events.js` ### Done when - [ ] A delivery returns quickly and is processed asynchronously - [ ] A redelivery produces one run - [ ] A bad signature is rejected - [ ] A wrong path returns nothing informative ### Watch out - Processing synchronously inside the webhook request means a slow action causes the source to time out and redeliver, which produces exactly the duplicate you were avoiding. Acknowledge immediately, queue the work, and let the idempotency handle the redelivery that will happen anyway. ## M5 · Secrets outside the rules A rule can be exported and shared without leaking a token. ### Steps 1. Reference secrets by name Configs never contain values. Grep an export to confirm. Files: `src/secrets.js` 2. Prompt on import List what is needed. A rule that fails with a service authorisation error is hard to diagnose. Files: `src/import.js` 3. Encrypt at rest With the key outside the database. Losing it loses the secrets, which is the right failure. Files: `src/crypto.js` ### Done when - [ ] An exported rule contains no secret value - [ ] Import prompts for required secrets - [ ] A missing secret produces a clear named error - [ ] Secrets are encrypted at rest ### Watch out - A rule containing an inline token gets exported to share an automation and the token goes with it, usually into a chat message. Reference by name from the start, because retrofitting means auditing every rule anyone has already exported. ## M6 · Running it continuously (production only) Rules run without supervision and you find out when one stops. ### Steps 1. Run as a service with durable cursors Restart safe. A cursor lost on restart replays or skips, and both are bad. Files: `src/service.js` 2. Alert on silence and on failure Expected frequency per rule. Silence is the failure you will otherwise miss. Files: `src/health.js` 3. Back up and restore Rules, cursors and secrets. Without the key the secrets are gone, which is deliberate. Files: `scripts/backup.sh`, `README.md` ### Done when - [ ] The service survives a restart with cursors intact - [ ] A silent rule produces an alert - [ ] Repeated failures alert rather than retrying forever - [ ] A restore reproduces rules, cursors and secrets ### Watch out - A rule that silently stops firing is indistinguishable from a rule whose trigger simply has not occurred, and you notice weeks later when something did not happen. Record an expected frequency per rule and alert when it is not met, since silence is the failure mode here.
# Operations · IFTTT ## Backup Rules, cursors and the encrypted secret store, with the encryption key kept separately. A restore without cursors will replay or skip events depending on the trigger, so they are part of the data rather than a cache. ## Restore Restore all three, start the service and confirm a rule fires once. Cursors being correct is what makes that a clean restore rather than a burst of replays. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Per-rule run counts against expected frequency, failure rates, and ceiling trips. Silence is the failure mode that needs active detection, which is why expected frequency is configured per rule. ## Incident checklist A loop is the expensive incident and the ceiling is the control; if a loop ran past it, the ceiling was warning rather than disabling. A duplicate side effect means the event key was not derived from something stable, which is worth fixing at the trigger rather than downstream. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A repeated event is confirmed to produce one run via database constraint - [ ] A deliberately looping rule is confirmed to disable itself - [ ] No secret value appears in a log or an exported rule - [ ] A webhook redelivery is confirmed to produce one run - [ ] A silent rule is confirmed to produce an alert ## Launch constraint Do not market omitted IFTTT capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, events, runs and cursors. DB_PATH=./data/rules.db # Required · secret. Encrypts the secret store. Kept outside the database; losing it loses the secrets deliberately. SECRETS_KEY=generate-a-long-random-value # Required. Default ceiling per rule. Exceeding it disables the rule rather than warning. DEFAULT_MAX_RUNS_PER_HOUR=30 # Required. Where inbound endpoints live. Must be HTTPS. WEBHOOK_BASE_URL=https://hooks.example.com # Required. Floor on polling frequency. Politeness to the sources and a limit on your own request volume. POLL_MIN_INTERVAL_SECONDS=300 # Required · secret. Where ceiling trips, failures and silent rules are reported. ALERT_WEBHOOK=https://chat.example.com/hook
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because the applet is easier to click together than to maintain.
xconsumer integration catalog
xsmart-home integrations
xmobile applets
xeasy UI
xmaintenance when APIs change
Don't feel like building it? These folks already made it free.
all 4 free alternatives to IFTTT →· no votes, no pay-to-list · just what's real
IFTTT pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 2 Applets; unlimited Applet runs; standard trigger speeds. |
| pro | $4.99 | $2.99 | 20 Applets; multi-action Applets; faster trigger speeds. |
| pro+ | $14.99 | $8.99 | Unlimited Applets; multiple accounts per service; developer tools; AI services. |
free tier2 Applets and unlimited runs; no time limit.
billingmonthly + annual (annual displayed at about 40% lower effective monthly price); auto-renews until cancelled
hidden costsNo general task overage is published; Pro+ AI services are rate-limited to 200 requests per day, so higher recurring use cannot simply be bought as metered overage.
verified 2026-08-11 · source ↗
Vibecode IFTTT
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal IFTTT replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does IFTTT cost?
IFTTT costs about $2.99/month (Pro, checked 2026-07-30), which is $35.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing IFTTT?
Honestly: consumer integration catalog; smart-home integrations; mobile applets; easy UI; maintenance when APIs change. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to IFTTT?
Yes: Home Assistant (IFTTT for the house, except the house keeps the data and needs an always-on box.) Huginn (IFTTT's paranoid older cousin: ugly, capable, and living on your server.) Activepieces Community Edition (App-to-app automations with schedules, webhooks, approvals, and no per-task meter.) All 4 curated free alternatives are at vibecodeit.com/ifttt/alternatives. The prompt is for when you want it exactly your way.