5 minute read

This Post walks through the hardening phase of the DataInsideData™ Builder Showcase intake pipeline project.

This is where the system moved from a foundation of:

Nice, this form works!

to:

The form works, protects the database, helps users catch mistakes, and blocks obvious duplicate/spam behavior.

That was a meaningly shift.

The pipeline looks like this:

Jekyll / static form
    ↓
preview-before-submit
    ↓
Supabase Edge Function
    ↓
validation + duplicate checks
    ↓
Postgres table insert
    ↓
review queue
    ↓
promotion / publishing workflow

This guide is written for folks learning full-stack systems, static sites, Supabase, and backend validation. It is also practical enough for production-minded builders who want to avoid the classic mistake of maybe trusting the browser a bit too much.

Why frontend validation is not enough

Frontend validation is helpful because it gives users fast feedback.

But frontend validation is not the source of truth.

Anything in the browser can be bypassed.

That means this is good:

HTML required fields
JavaScript preview checks
friendly error messages

But this is necessary:

Supabase Edge Function validation
backend duplicate checks
backend honeypot check
backend API key check

The rule I use:

Frontend validation improves the experience.
Backend validation protects the system.

Add preview-before-submit

The preview step improves quality before the data reaches Supabase.

The workflow becomes:

fill form
    ↓
preview cleaned values
    ↓
make edits if needed
    ↓
confirm and submit

This catches:

misspelled title
wrong repo URL
missing tags
missing required fields, etc...

When the user clicks Make Edits, send focus back to the form:

if (editButton) {
  editButton.addEventListener("click", function () {
    previewPanel.hidden = true;
    pendingPayload = null;

    status.textContent = "Make your edits, then preview again.";

    const firstField = document.getElementById("first_name");

    form.scrollIntoView({
      behavior: "smooth",
      block: "start"
    });

    window.setTimeout(function () {
      if (firstField) {
        firstField.focus({ preventScroll: true });
      }
    }, 400);
  });
}

That small UX detail matters. It keeps users from getting stranded at the bottom of the page.

  • The preview page needs to disappear after form submission, which will be addressed in the other round of modifications.

Require the right fields

The form should require:

first name
last name
email
repo URL, etc...

Optional fields should be labeled:

email (Optional)
---

## 4. Use strong acknowledgement language

The ownership checkbox should be specific:

```text
I confirm that I own this project, contributed to it, or have permission to submit it for review.

The public showcase checkbox should also be intentional:

I understand that, if approved, this project will be displayed publicly on the DataInsideData™ Builder Showcase...
  • These boxes should not be pre-checked.
  • The user should actively acknowledge them.

Add a honeypot field

A honeypot is a field real users should never fill.

Bots may fill it because they see it in the HTML.

Frontend Div Block:

<div class="did-honeypot" aria-hidden="true">
  <label for="website">Website</label>
  <input
    type="text"
    id="website"
    name="website"
    autocomplete="off"
    tabindex="-1"
  >
</div>

I used this SCSS styling, keeping hidden for seamless UX:

.did-honeypot {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

Backend filtering:

if (cleanString(payload.website)) {
  errors.push("Invalid submission.");
}

That provides a simple first-layer bot filter.

Validation using the Edge Function

The Edge Function should validate a number of key targets:

required fields
email shape
category allowlist
tag count
checkbox acknowledgements
honeypot, etc...

Example repo URL validation:

function isValidGitHubRepoUrl(value: string): boolean {
  try {
    const url = new URL(value);

    const isGithubHost =
      url.hostname === "github.com" || url.hostname === "www.github.com";

    const parts = url.pathname
      .split("/")
      .map((part) => part.trim())
      .filter(Boolean);

    return isGithubHost && parts.length >= 2;
  } catch {
    return false;
  }
}

Store request metadata carefully

For basic operational visibility, the Edge Function can store:

user_agent
request_origin
request_referer
client_ip_hash

Example added columns:

Interaction with Supabase table is achieved using psycopg2, pgadmin, and postgresql

alter table public.builder_project_submissions
add column if not exists user_agent text,
add column if not exists request_origin text,
add column if not exists client_ip_hash text;

I prefer storing an IP hash instead of raw IP Address.

That means the system can recognize repeated behavior later without casually storing the plain network address.

I Need a Salt, but What is it?

A salt is a secret random value mixed into another value before hashing.

Instead of storing:

123.45.67.89

hash it instead:

SECRET_SALT:123.45.67.89

That makes the stored hash harder to guess or reverse by precomputing common values.

Generate a salt using command:

python -c "import secrets; print(secrets.token_hex(32))"

And set it as a Supabase secret:

npx supabase secrets set DID_IP_HASH_SALT="PASTE_RANDOM_HEX_STRING"

In the Edge Function:

const salt = Deno.env.get("DID_IP_HASH_SALT");

Privacy note: this is still security/anti-abuse metadata. It will be treated intentionally and documented in the privacy policy when implemented.

It’s important to avoid heavy-handed blocking

So do not block:

same email alone
same repo URL alone
same IP hash alone

Why?

Because productive builders may make multiple submissions.

And monorepos are real—I know I have one.

So the better rule is:

same actual submission evidence

And not the:

same person

Add duplicate checks

Duplicate rules:

same email + same repo URL + same project title within 10 minutes
    → block

IP hash
    → store for future analysis

Use 409 Conflict for duplicate submissions.

That is the balance: stop duplicates, but encourage productive users.

Add browser cooldown

This is simply a UX helper, not the main security layer.

After successful submission:

localStorage.setItem(
  "did_builder_last_submission_at",
  String(Date.now())
);

Before confirming submission:

const lastSubmissionAt = Number(
  localStorage.getItem("did_builder_last_submission_at") || "0"
);

const cooldownMs = 60 * 1000;

if (lastSubmissionAt && Date.now() - lastSubmissionAt < cooldownMs) {
  status.textContent =
    "Please wait a moment before making another submission.";
  return;
}

This prevents rapid accidental resubmits from the same browser.

Deploying the Edge Function

First, run a quick check in the supabase project function folder:

cd supabase/functions/submit-builder-project
deno check index.ts

Deploy:

npx supabase functions deploy submit-builder-project

If the CLI asks for login:

npx supabase login

IF the browser doesn’t execute / appear automatically

  • Copy the address from terminal and paste into browser.
  • Then sign into supabase account
  • Return to terminal
  • Select the desired project from the terminal list

Then deploy again.

Test is Key

Test these:

valid form
unchecked ownership
unchecked public showcase
blank category
blank tags
bad repo URL
honeypot filled
same email + repo + title duplicate
same email + same repo + different title, etc...

The goal is not to block productive users.

The goal is to block bad payloads, obvious bots, and duplicate project submissions.

Final takeaway

This phase is where the system starts to feel real.

The form is not just collecting data anymore.

Its guiding the user, protecting the backend, preserving trust, and keeping the workflow useful for all.

That is the difference between:

a form

and:

an intake system

Data Inside Data™.

Tech Hands, a Science Mind, and a Heart for Community.

Updated: