mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 12:23:50 +00:00
Subscribing takes one click and cancelling takes a support ticket, and no reader can check that the two are as easy as each other because the way out is not written down anywhere a reader can fetch. The service already has a plan table, a cancel endpoint and a refund rule. OpenSaaS is those at /.well-known/opensaas.json: plans, and eight actions (subscribe, cancel, pause, resume, change_plan, unsubscribe, export, delete), each as the page a person opens and the endpoint an agent calls with an OpenAccess scope, with steps and confirm as the exit measure a directory shows beside the entrance. Doc, landing page, registry entry under catalogs. nichedb.dev is named as the first directory and the first service. Claude-Session: https://claude.ai/code/session_01S7yeJUHGxA4P5N74xnsRPQ Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
206 lines
9.9 KiB
TypeScript
206 lines
9.9 KiB
TypeScript
import Link from "next/link";
|
|
import type { ReactNode } from "react";
|
|
import type { Metadata } from "next";
|
|
import { SiteShell } from "@/components/site-shell";
|
|
import { mono, pre, table, td, th } from "../openontology/ui";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "OpenSaaS · LogicSRC",
|
|
description:
|
|
"OpenSaaS is one file a subscription service serves at /.well-known/opensaas.json about how to deal with it: its plans, and how to subscribe, cancel, pause, change plan, stop the mail, export and delete, each as the page a person opens and the endpoint an agent calls with an OpenAccess scope. The way out, written down beside the way in.",
|
|
alternates: { canonical: "/opensaas" }
|
|
};
|
|
|
|
const DESCRIPTOR = `{
|
|
"service": { "name": "NicheDB", "web": "https://nichedb.dev",
|
|
"openaccess": "https://nichedb.dev/.well-known/openaccess.json",
|
|
"terms": "https://nichedb.dev/terms", "currency": "USD" },
|
|
"updated": "2026-09-13T06:00:00Z",
|
|
"plans": [
|
|
{ "id": "premium", "name": "Premium", "price": 1, "period": "day", "renews": true,
|
|
"url": "https://nichedb.dev/premium", "includes": ["no ads", "no tracker"] },
|
|
{ "id": "pro", "name": "Pro", "price": 30, "period": "month", "trial": "14 days",
|
|
"renews": true, "url": "https://nichedb.dev/pro" }
|
|
],
|
|
"actions": {
|
|
"subscribe": { "page": "https://nichedb.dev/premium",
|
|
"api": { "method": "POST", "url": "https://nichedb.dev/api/v1/billing/subscribe" },
|
|
"scope": "billing:subscribe", "steps": 2, "requires": ["account", "payment"] },
|
|
"cancel": { "page": "https://nichedb.dev/account/billing",
|
|
"api": { "method": "POST", "url": "https://nichedb.dev/api/v1/billing/cancel" },
|
|
"scope": "billing:cancel", "steps": 1, "confirm": "click",
|
|
"effective": "period-end", "refund": "none" },
|
|
"unsubscribe": { "page": "https://nichedb.dev/account/mail", "scope": "mail:unsubscribe",
|
|
"api": { "method": "POST", "url": "https://nichedb.dev/api/v1/mail/unsubscribe" },
|
|
"steps": 1, "list_unsubscribe": true },
|
|
"export": { "page": "https://nichedb.dev/account/export", "scope": "account:export",
|
|
"api": { "method": "POST", "url": "https://nichedb.dev/api/v1/account/export" },
|
|
"steps": 1, "formats": ["json"], "within": "P1D" },
|
|
"delete": { "page": "https://nichedb.dev/account/delete", "scope": "account:delete",
|
|
"api": { "method": "POST", "url": "https://nichedb.dev/api/v1/account/delete" },
|
|
"steps": 2, "confirm": "email", "effective": "immediate", "retention": "P30D" }
|
|
},
|
|
"policies": { "auto_renew": true, "renewal_notice": "P7D", "price_change_notice": "P30D" }
|
|
}`;
|
|
|
|
const ACTIONS: Array<[string, string, string]> = [
|
|
["subscribe", "start a plan", "requires: account, payment, card, invoice, approval"],
|
|
["cancel", "end the plan", "effective: immediate or period-end; refund: none, prorated, full, P14D, or a URL"],
|
|
["pause", "stop paying for a while", "max: the longest pause, as a duration"],
|
|
["resume", "come back from a pause", ""],
|
|
["change_plan", "move to another plan", "proration: immediate, period-end, none"],
|
|
["unsubscribe", "stop the mail, not the plan", "list_unsubscribe: true when RFC 8058 headers are in the mail"],
|
|
["export", "take your data", "formats, and within: how long it takes"],
|
|
["delete", "close the account", "retention: how long the data lives after"]
|
|
];
|
|
|
|
const BOTH: Array<[string, string]> = [
|
|
["page", "The URL a person opens. The human half of the action, and the one place the way out is written down beside the way in."],
|
|
["api", "{ method, url } an agent calls with an OpenAccess bearer. Answers done, scheduled, pending with a next, or refused with a reason."],
|
|
["scope", "The OpenAccess scope the bearer must carry, defined in the service's own openaccess.json with a line a person reads on the consent screen."],
|
|
["steps", "How many things a person does from page to done. cancel.steps against subscribe.steps is the number a directory shows first."],
|
|
["confirm", "What the service demands before it takes: none, click, email, password, chat, call, mail. The last three are the ones a directory flags."]
|
|
];
|
|
|
|
const ABSENT: Array<[string, string]> = [
|
|
["No checkout", "subscribe.api starts a plan for a person with a way to pay on file, or answers pending with the page to add one. Payment is the processor's business."],
|
|
["No retention offers", "A discount on the way out is offered on page, to a person. cancel.api cancels."],
|
|
["No account data", "The file describes actions, not the account. The current plan comes from the service's API under the reserved entitlements scope."],
|
|
["No badges", "steps and confirm are the service's own statements. A directory that scores exit ease labels the score as its own and shows the numbers."]
|
|
];
|
|
|
|
export default function OpenSaaSPage(): ReactNode {
|
|
return (
|
|
<SiteShell active="OpenSaaS">
|
|
<div className="band">
|
|
<div className="section-head">
|
|
<p className="eyebrow">LogicSRC standards surface</p>
|
|
<h2>OpenSaaS</h2>
|
|
<p>
|
|
One file a subscription service serves about how to deal with it: the way in and the
|
|
way out of every plan, for a person and for an agent, in the same place.
|
|
</p>
|
|
</div>
|
|
<p style={{ color: "#41505d" }}>
|
|
Subscribing takes one click. Cancelling takes a support ticket, a phone call in office
|
|
hours and a retention screen. The way in is on the pricing page; the way out is nowhere a
|
|
search finds, and an agent asked to cancel what a person does not use cannot. The service
|
|
already has a plan table, a cancel endpoint and a refund rule. OpenSaaS is those, exported,
|
|
at <code style={mono}>/.well-known/opensaas.json</code>: every action as the{" "}
|
|
<code style={mono}>page</code> a person opens and the <code style={mono}>api</code> an
|
|
agent calls with an <Link href="/openaccess">OpenAccess</Link> scope.
|
|
</p>
|
|
<p style={{ color: "#5b6b7a" }}>
|
|
Status: 0.1. The first directory reading it is the saas collection at{" "}
|
|
<a href="https://nichedb.dev/c/saas">nichedb.dev</a>, and the first service serving one
|
|
is nichedb.dev itself. The smallest valid file is a service with a name and one action
|
|
with a page.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="band">
|
|
<div className="section-head">
|
|
<h2>The descriptor</h2>
|
|
<p>
|
|
A cancel endpoint fetched from the service's own origin is a cancel endpoint the
|
|
service says works. That is the whole verification.
|
|
</p>
|
|
</div>
|
|
<pre style={pre}>{DESCRIPTOR}</pre>
|
|
</div>
|
|
|
|
<div className="band">
|
|
<div className="section-head">
|
|
<h2>Eight actions</h2>
|
|
<p>
|
|
<code style={mono}>unsubscribe</code> is about mail and <code style={mono}>cancel</code>{" "}
|
|
is about the plan, and they are two actions because services conflate them on purpose.
|
|
Unknown actions are kept under their own name.
|
|
</p>
|
|
</div>
|
|
<table style={table}>
|
|
<thead>
|
|
<tr>
|
|
<th style={th}>action</th>
|
|
<th style={th}>what it does</th>
|
|
<th style={th}>its own keys</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{ACTIONS.map(([action, what, keys]) => (
|
|
<tr key={action}>
|
|
<td style={td}>
|
|
<code style={mono}>{action}</code>
|
|
</td>
|
|
<td style={td}>{what}</td>
|
|
<td style={td}>{keys}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="band">
|
|
<div className="section-head">
|
|
<h2>One action, both readers</h2>
|
|
<p>
|
|
Every action carries the same five keys. An action with a page and no api can only be
|
|
done by a person; one with an api and no page only by an agent; a directory shows which.
|
|
</p>
|
|
</div>
|
|
<table style={table}>
|
|
<tbody>
|
|
{BOTH.map(([key, what]) => (
|
|
<tr key={key}>
|
|
<td style={td}>
|
|
<code style={mono}>{key}</code>
|
|
</td>
|
|
<td style={td}>{what}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="band">
|
|
<div className="section-head">
|
|
<h2>What is deliberately absent</h2>
|
|
</div>
|
|
<table style={table}>
|
|
<tbody>
|
|
{ABSENT.map(([what, why]) => (
|
|
<tr key={what}>
|
|
<td style={td}>
|
|
<strong>{what}</strong>
|
|
</td>
|
|
<td style={td}>{why}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="band">
|
|
<div className="section-head">
|
|
<h2>Where everything lives</h2>
|
|
</div>
|
|
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
|
|
<li>
|
|
<Link href="/docs/opensaas">Specification</Link>: the descriptor, ten rules, eight
|
|
actions, acting on one as a person or an agent, discovery, what a directory owes a
|
|
service
|
|
</li>
|
|
<li>
|
|
<a href="https://nichedb.dev/.well-known/opensaas.json">nichedb.dev/.well-known/opensaas.json</a>
|
|
: the first descriptor, and <a href="https://nichedb.dev/c/saas">nichedb.dev/c/saas</a>,
|
|
the first directory reading them
|
|
</li>
|
|
<li>
|
|
<Link href="/openaccess">OpenAccess</Link>, the grant an agent carries to call an action;{" "}
|
|
<Link href="/opencoupon">OpenCoupon</Link>, the same idea for a merchant's promotions
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</SiteShell>
|
|
);
|
|
}
|