# Design note — custom profile fields

**Dated 2026-08-05. Written before the code, per the provenance practice in
[ROADMAP.md](../../ROADMAP.md#proving-independence).**

---

## The job to be done

Every community wants to know something specific about its members that no
forum ships with. A guitar forum wants "what do you play". A support community
wants "which version are you on". A local group wants "where are you". Convoro
offers a title and a signature and nothing else, so the answer ends up in
people's signatures, unsearchable and unformatted.

## Decision 1 — definitions in one table, values in another

`profile_fields` describes what exists; `profile_field_values` holds what each
member said.

Not a JSON blob on `users`. A blob cannot be indexed, cannot be validated per
field, and turns "who is in Berlin" into a full table scan and a regex. Two
tables is the boring answer and it is the right one.

## Decision 2 — the key is immutable, the label is not

A field has a **key** — set once, never changed — and a **label** the operator
can rename freely.

Renaming a key would orphan every value pointing at it. Renaming a label is
what operators actually want to do, and it costs nothing.

## Decision 3 — a small, closed set of types

Text, textarea, select, checkbox, url, number, date. That is enough to describe
a person and small enough that every type has a real validator behind it.

The temptation is a "custom regex" type. It produces fields nobody can fill in
correctly and error messages nobody can act on, and the operator who wanted it
wanted a select.

## Decision 4 — visibility defaults to members, not to the world

Each field is visible to **everybody**, to **signed-in members**, or to
**staff**.

The default is members. These are the fields where people put their location,
their real name and their other accounts, and a public profile page is a
harvesting surface. An operator who wants a public directory can say so per
field; one who never thinks about it does not accidentally publish where their
members live.

Staff-only exists for the other direction — a note on an account that the
account holder should not be editing.

## Decision 5 — who may fill it in is separate from who may see it

A field is editable by its **owner** or by **staff only**.

"Verified", "Team role", "Ticket reference" are things a community sets *about*
somebody. Conflating that with visibility would mean any field staff maintain
is also a field only staff can read, which is wrong in both directions.

## Decision 6 — beside posts is opt-in, and it is a different question

A field can be marked to show in the post sidebar. Most should not be: the
sidebar is read on every post of every thread, and four extra rows there costs
more attention than the same four rows on a profile nobody has to visit.

So it is off by default and per field.

## Decision 7 — these are personal data, and the rest of the system already knows what that means

Values are:

- included in the **data export**, because they are things the site holds about
  somebody;
- deleted on **erasure**, along with everything else personal;
- carried in the **profile settings** form the member already uses, not a
  second page somewhere else.

Wiring this in at the same time is the point. A feature that adds personal data
and does not tell the export about it quietly breaks a subject access response
months later, and nobody notices until it matters.

## Decision 8 — deleting a field takes its answers with it

There is no "hide the field but keep the data". If the community has stopped
asking, it has stopped holding the answers, and the confirmation says so.

## What this does not do

- **No per-group fields.** "Only staff have this field" is a visibility and
  editability question, which is already answered.
- **No conditional fields.** "Show this only if that is set" is a form builder,
  and this is a profile.
- **No search by field.** Worth doing once there is a search driver worth
  querying; the schema is ready for it.
- **No custom regex validation.** See decision 3.
