1. The story we keep hearing
Every team I talk to about Bugspot has roughly the same story. They started on something thin — Trello, GitHub Issues, a Notion database somebody spun up on a Friday — and it worked great until it didn't. Then somebody migrated them to Jira, or they migrated themselves to Linear, and the pain rotated rather than disappeared.
The Jira teams complain about the admin screen: who can move what, into which state, under which conditions, with which custom field requirements. The Linear teams complain about the opposite: a beautiful tool that has opinions about how software ships, some of which are not theirs. Both complaints are about the same axis. One tool let process eat schema; the other fixed both at the same time.
2. What 'fixed schema' really means
When we say "fixed schema" we mean it precisely. Every Entry in Bugspot — bug, task, story, epic, milestone — has the same set of top-level fields, and that set is closed. You don't add to it. We don't add to it. The shape of an Entry is part of the contract.
The shape:
title— a short string, never emptydescription— markdown, optionaltype— one ofbug,task,story,epic,milestone,initiativestatus— workspace-defined; pluggable per-workspacepriority,severity— closed enums, mapped per-workspace labelassignee,reporter,labels,links,comments
That's it. There is no due_date_2_for_compliance. There is no
"Customer Impact (Q1 framework)" picklist. If you need to track that, the
answer is a label, a link, or a different tool. We are honest about which one.
What you get for free
Because the schema is fixed, the rest of the product can lean on it. Search, views, the API, exports, integrations — none of them have to ask the per-tenant schema service "what does an Entry look like in this workspace today?" They know. That changes more about the product than I expected when we drew the line.
3. The line between schema and process
Process, by contrast, is wide open. You define your statuses, your transitions, your role-based permissions, your QA gates, your release rules. The structure of an Entry doesn't change, but the way an Entry moves through your team is entirely yours.
This is the part that I've found people miss when they hear "fixed schema": they assume we mean a fixed workflow. We don't. Workflow is the part that actually differs between teams. Fixing it would be the wrong move.
The schema is a contract. The process is a configuration. Trackers that confuse the two end up either too rigid (Linear, on process) or too loose (Jira, on schema). The correct answer is to be strict about one and generous about the other.
4. A workflow, drawn
Here's the default workflow we ship with the SaaS template. Every node is a status; every edge is a transition with optional permission rules. You can edit it freely — but the schema underneath stays exactly the same.
Notice what isn't in the diagram. There's no implicit field that gets set when you cross an edge. No "QA-Approved-By" field that mysteriously appears in the in_review column. The transition is the event; the schema is the noun. The tracker that mixes them ends up with a hundred shadow fields.
5. How it's enforced
The contract is enforced at three layers: the database, the API, and the client. The database is the load-bearing one. Here's a stripped-down version of the entries table — actual migration, mostly:
-- Entries are the universal noun in Bugspot.
-- Every list, board, search, and saved filter is over this table.
CREATE TYPE entry_type AS ENUM (
'bug', 'task', 'story', 'epic', 'milestone', 'initiative'
);
CREATE TABLE entries (
id bigserial PRIMARY KEY,
workspace_id bigint NOT NULL REFERENCES workspaces(id),
type entry_type NOT NULL,
title text NOT NULL CHECK (length(title) <= 200),
description text,
status_id bigint NOT NULL REFERENCES statuses(id),
priority smallint NOT NULL CHECK (priority BETWEEN 0 AND 4),
assignee_id bigint REFERENCES users(id),
created_at timestamptz NOT NULL DEFAULT now()
);
-- No JSONB "custom_fields" column. On purpose.
That's the whole story. There's no custom_fields jsonb sitting
quietly at the bottom waiting for someone to plumb it through. The thing that
doesn't exist in the migration won't exist in the API, the search index, the
export, or the third-party integration that rebuilds the schema for you. We
removed a lot of code by not writing it.
On the API side, the contract is just as plain. Here's how a client creates an entry — and what the response looks like:
import { z } from "zod";
export const EntryInput = z.object({
type: z.enum(["bug", "task", "story", "epic", "milestone", "initiative"]),
title: z.string().min(1).max(200),
description: z.string().optional(),
priority: z.number().int().min(0).max(4),
labels: z.array(z.string()).max(12).default([]),
});
export async function createEntry(input: unknown) {
const parsed = EntryInput.parse(input);
return db.entries.insert(parsed);
} The schema lives in three places: the migration, the validator, and the type. Three sources of truth, all aligned, none of which has a "..." escape hatch. That's the property worth keeping.
What a request looks like over the wire
POST /v1/entries HTTP/1.1
Authorization: Bearer bgs_live_e8c3...
Content-Type: application/json
{
"type": "bug",
"title": "PDF export drops the cover page on Safari 17",
"priority": 2,
"labels": ["export", "safari"]
} 6. What we gave up
This is the honest part. Fixing the schema closes some doors. We do not get to sell to teams who want their tracker to also be their CRM, their compliance log, their content calendar, or their non-software intake form. Every quarter we lose a deal because somebody wants a "Region (EMEA / NA / APAC)" picklist and we won't add it.
The "we'll model anything" market
Agencies, ops teams, marketing, anyone who needs the tracker to also be a generic database. Jira and ClickUp keep that crowd; we don't try.
An interface that fits in your head
The product is small enough that a new engineer can be productive in an afternoon, and the schema is small enough that you can read it on one screen and trust it.
That's the trade. We are choosing to be a sharper tool that fits a smaller number of teams, rather than a generic one that fits everyone slightly badly.
7. Common pushback
"What if I need just one extra field?"
Use a label. Labels are first-class — searchable, filterable, color-coded — and they cover 90% of the cases that look like "we need a custom field." If the answer is "I need it to be a date" or "I need it to be a number," that's usually a sign the thing belongs in a different system.
"What about per-tenant required fields?"
Required transitions, not required fields. You can configure a transition
(say, in_review → shipped) to require a linked PR, an approver
from a specific role, or a non-empty release note. The constraint sits on the
edge in the workflow, not on the entry, and it doesn't multiply your schema.
"Will you ever change your mind?"
On this, no. We've turned down two acquisition conversations and one large customer over it. The product's identity is the line we drew. If we ever feel the urge to ship custom fields, we'll fork the product and call the new one something else.
If this position resonates, you're probably the right shape of team for Bugspot. See pricing → or read the positioning. If it doesn't — honestly, thank you for reading this far. The right tool is the one your team will still respect a year from now.
Vladimir Petrov
Solo developer behind Bugspot. Previously eight years on developer tools at companies you've heard of. Writes here about the parts of building a bootstrapped product that don't get a hashtag.