A habit tracker with friends can make a goal feel real for about three days. Then the shared chart becomes another place to avoid. The useful question is smaller: who gets to know when you missed, and what can they actually do about it?

A habit tracker with friends should pick one witness

Group habit tracking sounds motivating until everyone is performing for the room. PactBuddy takes the narrower route. A pact has one trusted buddy, an invite, and a signature from both people before the goal starts to matter inside the app.

That choice is not just copy. The backend surface is built around pairing two people and then keeping every read scoped to pacts they share.

func createPactWithInvite(name: String?, creatorId: UUID) async throws -> CreatedPact
func redeemInvite(code: String) async throws -> RedeemInviteResult
func signPact(pactId: UUID, userId: UUID) async throws

A social habit tracker usually asks, "who can see this?" PactBuddy asks a more useful question first: "who has agreed to be on the other side of this miss?" That makes the friend a participant, not an audience.

The goal has to be concrete before a buddy can help

"Be better" is a lousy shared goal. It gives your friend nothing to verify and gives you too many ways to argue. PactBuddy's custom goals still use a structured target, an operator, a cadence, and a local deadline.

public struct CustomGoalConfig: Codable, Sendable, Hashable {
    public var title: String
    public var operator: Operator
    public var targetValue: Int
    public var unit: String
    public var cadence: String
    public var targetCount: Int
    public var deadlineLocal: String
}

The product supports built in goals like wake up, workout, and Screen Time, but the same rule applies to custom promises. The goal needs a number, a deadline, and a proof path. A buddy cannot verify a vibe.

Proof is different from progress sharing

A lot of shared habit apps stop at visibility. Your friend can see a checkmark, a streak, or a comment. That is friendly, but it does not answer the hard question after a miss: what happened next?

PactBuddy turns proof into a typed action. The owner can submit a wake up selfie, a workout photo, a private Screen Time result, or a manual check in for a custom goal.

enum Method: String, Sendable, CaseIterable {
    case wakeSelfie = "wake_selfie"
    case photo
    case screentimeSync = "screentime_sync"
    case manual
}

The Screen Time version has its own privacy boundary, covered in the post on what a screen time accountability app should show a buddy. The broader point is the same for every goal type. A friend should get enough evidence to make a fair call, not a full diary.

A miss needs an owner, a state, and a next page

Most habit trackers are weakest right after failure. They record the red day and move on. PactBuddy opens an incident instead. A missed occurrence gets one pager ladder with an owner, a state, a page count, and the next time the page should fire.

create table public.incidents (
  id             uuid primary key default gen_random_uuid(),
  occurrence_id  uuid not null unique references public.occurrences (id) on delete cascade,
  owner_id       uuid not null references public.profiles (id) on delete cascade,
  pact_id        uuid not null references public.pacts (id) on delete cascade,
  state          public.incident_state not null default 'open',
  page_count     int not null default 0,
  next_page_at   timestamptz not null default now()
);

The notification system uses Apple's Time Sensitive interruption level, so this has to be an agreement the user actually opted into. That constraint is healthy. A habit tracker with friends should create pressure people consent to, not sneak pressure through a dark pattern.

The deeper mechanics are in our post on what an accountability partner app needs to do after you miss.

Public sharing should come after the pact

PactBuddy still has a social surface. Goal Wrapped can turn progress into a shareable card. The difference is timing. Sharing comes after the private agreement, not before it.

let goals = goalLabels.prefix(2).joined(separator: " + ")
let goalLine = goals.isEmpty ? "their goals" : goals.lowercased()
return "\(identity) is \(completedCount)/\(goalCount) on \(goalLine) today. Accountability hits different with a buddy."

The card uses goal labels and completed counts. It does not need the buddy's name, private proof, exact schedule, or failed day. A public win can be motivating. It should not become the reason the private pact works.

What this does not solve

A habit tracker with friends cannot make the friend reliable. It cannot rescue a goal that was chosen to impress someone else. It also should not turn friendship into surveillance. PactBuddy is betting on a narrower kind of pressure: one person, one promise, and a clear way to close the loop after the day goes sideways.

Start with the habit you keep explaining away

Pick the habit where a miss usually becomes a private excuse. Choose the person whose question would make that excuse harder to believe. Then make the proof obvious before the first deadline arrives.