An accountability pact is easy to make at 9 a.m. It earns its name at 9:07 p.m., when one person missed the goal and both people need to know what happens next.

PactBuddy uses a narrow version: one pact, two members, one concrete goal per member, and a proof handoff. The app gives the agreement a shape, then gives a missed day a state. That is the part most habit apps leave to a vague text message.

An accountability pact starts with a signed agreement

An accountability pact should answer two questions before the goal begins: who is on the other side, and what did both people agree to do? A group feed can make a promise visible. It does not make anyone responsible for closing the loop.

PactBuddy creates a pact in a forming state, adds the initiator, and mints an invite for the buddy. The invite expires after seven days. The database also rejects a third member, so the relationship stays a pair instead of turning into a small audience.

let expires = Date().addingTimeInterval(7 * 24 * 60 * 60)
let membership = PactMemberInsert(pactId: pact.id, userId: creatorId, role: .initiator)
_ = try await client.from("pact_members").insert(membership).execute()
_ = try await client.rpc("sign_pact", params: PactIDParams(pactId: pactId)).execute()

That structure changes the conversation. The other person is not just someone who can see a streak. They are the person who signed up to respond when the plan slips. The agreement is private, explicit, and small enough to remember.

A concrete goal gives the pact something to discuss

An accountability agreement cannot verify "be healthier" or "use my time better." Those are intentions. A buddy needs a target, a deadline, and a way to show what happened.

PactBuddy's custom goal model keeps those fields together. It supports an at-least or at-most operator, a value, a unit, a cadence, a number of occurrences, 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
}

That is why "work out three times by Sunday" gives a buddy more to work with than "get in shape." The goal can still be personal. It just cannot be so foggy that every check-in becomes an argument.

The same rule shows up in why a habit tracker with friends needs proof. Shared goals work better when the friend has one concrete thing to witness instead of another chart to interpret.

Private proof is part of the accountability pact

Proof should fit the goal and stop there. A wake-up selfie can confirm that a person got up. A workout photo can confirm that a session happened. A Screen Time result can confirm that a limit was met without exposing every app that was open.

The client represents those choices as a small enum. When an image exists, the live backend uploads it under the user's ID and occurrence ID, then passes the resulting path to the proof RPC. The buddy gets a check-in, not a copy of the owner's whole phone life.

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

_ = try await client.rpc(
    "submit_proof_v2",
    params: SubmitProofParams(
        occurrenceId: submission.occurrenceId,
        method: submission.method.rawValue,
        proofUrl: proofPath,
        screenTimeBucket: submission.screenTimeBucket,
        screenTimeEstimateMin: submission.screenTimeEstimateMin
    )
).execute()

The privacy boundary matters because an accountability pact is still a relationship between two people. The post on what a screen time accountability app should show a buddy covers the narrow Screen Time result in more detail. The short version is that proof should answer the agreed question, then stay out of the rest of the person's life.

A missed goal needs a next state, not a guilt trip

The pact is easiest to understand after a miss. PactBuddy does not treat a failed occurrence as the end of the story. It opens an incident and gives the two people a defined handoff.

The shared Swift contract names the states directly:

public enum IncidentState: String, Codable, CaseIterable, Sendable {
    case open
    case awaitingVerify = "awaiting_verify"
    case resolved
    case excused
    case expired
}

The owner submits proof, which moves an open incident to awaiting_verify. The buddy can confirm it, reject it, or use a weekly vouch. Confirmation resolves the incident. Rejection puts it back in open, where the reminder loop can resume. A vouch moves it to excused and preserves the shared streak.

That is a better failure model than painting one day red and hoping the person feels bad enough to start over. It also gives the buddy something useful to do. Their job is not to deliver a verdict from a distance. Their job is to close, reopen, or excuse the specific incident in front of them.

The same handoff is the focus of what an accountability partner app needs to do after you miss. An accountability pact only has a point if the hard part is already agreed when the hard day arrives.

The buddy needs a boundary too

Pressure without consent is just noise with better branding. The app stores the user's exact consent text for paging, supports quiet hours, and lets the owner stop an open pager. Those controls make the pact more trustworthy because either person can see what the reminder loop is allowed to do.

The client keeps the consent record explicit. The first notification can use Apple's Time Sensitive interruption level, while the person still controls whether that channel is allowed:

public struct ConsentInsert: Codable, Sendable, Hashable {
    public var userId: UUID
    public var channel: String
    public var granted: Bool
    public var exactText: String
}

The backend also exposes stop_own_pager for the owner and stores a reason when an owner snoozes a page. A pact can ask for follow-through without pretending that a person has no job, family, illness, or bad day outside the app.

The boundary is part of the agreement, not a settings footnote. If the only way to keep someone accountable is to remove their ability to pause the system, the agreement was weak before the first notification.

What this accountability pact does not solve

An accountability pact cannot make a distant friend attentive. It cannot turn a goal chosen to impress someone else into a goal you actually want. It also cannot decide whether a blurry workout photo deserves a yes. That judgment belongs to the two people who made the agreement.

The product keeps the technical limits visible too. A pact can be forming, active, paused, or dissolved. A reminder loop has a floor of 900 seconds in the database, and an incident can expire after its allowed window. The system has boundaries because a person needs them.

status text not null default 'forming'
  check (status in ('forming', 'active', 'paused', 'dissolved')),
page_interval_sec int not null default 900

That is enough machinery to make an agreement legible. It is not a substitute for choosing a good buddy or asking for help when the goal itself needs to change.

Start with the smallest agreement you can keep

Pick one goal you keep explaining away. Ask one person to sign a specific version of it with you. Decide the proof, the deadline, and the response to a miss before either of you needs the answer.

Then check the pact after the first bad day. If both people knew what to do, the agreement is doing its job. If you had to improvise, change the pact before you change the person.