Build LnkFlow

KINDAreplaces $10.39/mosaves $124.68/yrback to the verdict

0%0 of 16 items done

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

A short link service whose hard problems are logging a click without slowing the redirect, joining a sale that happens days later on a different device back to the click that caused it, and getting that conversion out to an ad platform before the platform's import window closes, which is a deadline nobody warns you about.

estimated effort weekendthe files for this build are in the project pack

EdgeA single edge worker on your own subdomain, with the slug map in key-value storageClick logA relational store at the edge, written after the response is handed backIdentityA first-party cookie on your own domain plus a server-side click recordAttributionAn explicit join table with a deadline column, not a view over two tablesUploadA queue with retries, deduplication keys and a dead letter state

Before step 1

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

  1. free tiers cover a personal volume comfortably

    Why The redirect needs to run close to the visitor and the click log needs somewhere durable to land.

    Get it Create the account, the key-value namespace and the database, and install the platform's command line tool.

    Verify A deployed worker returns a redirect from a slug you created

  2. have readythe price of a domain

    Why A first-party cookie has to be set on a domain you own, and the whole identity design rests on that.

    Get it Point a record at the worker following the platform's instructions.

    Verify The short domain resolves and sets the cookie

  3. API keyfree to obtain; approval can take days

    Why Without an upload path the build stops at a dashboard, which is the part that was never worth paying for.

    Get it Create developer credentials in the platform's console and request whatever access level conversion upload requires.

    Verify A test conversion appears in the platform's own reporting

  4. decidefree

    Why The deadline is computed at click time from these numbers, so a wrong one silently expires conversions months later.

    Get it Read each platform's own documentation on offline conversion import and record the window and the date you read it.

    Verify Every platform in the configuration has a window and a checked-on date

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- links(slug, destination, utm_json, created_at, archived_at, owner_note)
- clicks(id, slug, at, country, user_agent_hash, referer, click_ids_json, visitor_id, import_deadline_at) · the deadline is computed at click time, not at conversion time
- visitors(visitor_id, first_seen_at, last_seen_at) · a first-party identifier you set, nothing borrowed
- conversions(id, at, kind, value_minor, currency, order_ref, visitor_id, matched_click_id, match_method, reversed_at)
- uploads(id, conversion_id, platform, attempt, status, dedup_key, request_summary, response_code, response_excerpt, at)
- expired(id, conversion_id, reason, noticed_at) · conversions that could never be uploaded, counted and shown rather than dropped
- The deadline lives on the click, because it is measured from the click. Storing it on the conversion means computing it from a timestamp you may no longer have.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
SHORT_DOMAINrequiredgo.example.comThe domain the worker answers on and the cookie is set for.
CONVERSION_API_TOKENsecretrequiredredactedAuthenticates your own application calling the conversion endpoint.
ADS_PLATFORM_SECRETsecretrequiredredactedAd platform credentials. Kept in the platform secret store, never in the worker config file.
PLATFORM_CONFIG_PATHrequired./config/platforms.jsonImport windows and their checked-on dates.
COOKIE_TTL_DAYSrequired90How long a visitor identifier survives. Keeping it longer than the import window buys nothing.
ADMIN_PASSWORDsecretrequiredredactedThe only gate on the link editor and the dashboard.

The build, in order

  1. A redirect that does not wait for anything

    A request to your domain resolves and redirects immediately, and the click is logged afterwards.

    1. Latency here is paid by every single visitor.

      Files src/worker/redirect.tstests/redirect-latency.test.ts

    2. Rewriting one of them is how attribution dies quietly.

      Files src/worker/params.ts

    3. A default destination turns a typo into traffic you cannot explain.

      Files src/worker/resolve.ts

    done when · tick each as it passes
    watch out
    • Awaiting the database write before returning the redirect adds the write latency to every click, which is invisible in testing and obvious on a mobile network in another continent, and it is the reason people abandon a link. Return the response first and complete the write after, then measure the redirect at the edge rather than from your own laptop.
  2. A visitor identity you own

    A click can be recognised again later from your own domain, without borrowing anybody else's identifier.

    1. Borrowing one couples your join to somebody else's format.

      Files src/worker/visitor.ts

    2. It is measured from the click and nothing else knows it.

      Files src/db/schema.sqltests/deadline.test.ts

    3. The count still matters even when the join cannot happen.

      Files src/worker/consent.ts

    done when · tick each as it passes
    watch out
    • Deriving the visitor identifier from the ad click identifier saves a cookie and then breaks the day a visitor arrives from an email instead of an advertisement, because those clicks now share whatever fallback you chose and the join produces confident nonsense. Generate an opaque identifier of your own and store the ad identifiers as separate fields that may be absent.
  3. Joining a sale to a click days later

    A conversion reported from your own application finds the click that caused it, or admits it cannot.

    1. An unlabelled match is indistinguishable from a guess.

      Files src/attribution/match.tstests/match.test.ts

    2. The count is the honest number and people need to see it.

      Files src/attribution/store.ts

    3. A retried webhook should not double your revenue.

      Files src/api/conversion.ts

    done when · tick each as it passes
    watch out
    • Falling back to the most recent click within some window when nothing matches produces attribution that looks complete and is partly invented, and because it is never obviously wrong it quietly steers the bidding. Record the match method on every conversion, keep unmatched ones visible with their own count, and never let a fallback write a match_method of anything but the rule that actually fired.
  4. The import window is a deadline, not a preference

    Matched conversions reach the platform inside its window, and the ones that miss it are counted rather than lost.

    1. These numbers change and a constant will not tell you.

      Files config/platforms.jsonsrc/upload/windows.ts

    2. First in, first out is the wrong order when things expire.

      Files src/upload/queue.tstests/queue-order.test.ts

    3. An expired conversion is money the bidder never learned about.

      Files src/web/dashboard.ts

    done when · tick each as it passes
    watch out
    • A nightly upload job that retries failures forever looks healthy and quietly accumulates conversions past their window, and because a rejected import is not an error in your own system the dashboard keeps showing revenue the bidder never saw. Order the queue by deadline, move anything past it into an expired table with a reason, and put that count on the front page next to the matched total.
what this build does not replace
after v1, if you want it

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