Two files.
One register line.
This is the complete contract for shipping a tool on tiredof.app. Read it once and you'll know everything the platform expects from your code.
`import * as` line in _registry.tsand you're live.A Dashlet is a manifest plus an implementation.
The manifest is the metadata - what the tool is called, what it costs, what it does. The implementation is a React component that runs in the user's browser. The platform handles everything between those two things and the user.
What the tool is
Slug, title, tagline, category, keywords, pricing, status, author. Validated by Zod at build time, so a typo fails CI. Ships as portable data - the same manifest powers the future iOS app.
What the tool does
A default-exported React component. Runs entirely client-side. Use any browser API, any client-side library, any state you want. Most Dashlets never touch a server.
Five steps. End-to-end.
Concrete walkthrough using a hypothetical pdf-merger Dashlet.
Want a head start?
Grab a zip with both files filled in and a README. Drop it into your fork and edit.
Create the folder
Every Dashlet lives in its own folder under dashlets/. The folder name doesn't matter - the slug in your manifest does. Convention is to match them.
mkdir dashlets/pdf-merger
Write the manifest
This file declares your Dashlet to the platform. Two exports required: manifest and load. Forget either and the build refuses to compile.
import type { DashletManifest } from "../_schema"; export const manifest: DashletManifest = { slug: "pdf-merger", title: "PDF Merger", tagline: "Drop PDFs. Get one PDF.", description: "Combine PDFs in your browser. Nothing uploads.", category: "pdfs", keywords: ["pdf", "merge", "combine", "join"], status: "live", pricing: { kind: "free" }, author: "tiredof", platforms: ["web"], // or ['web','ios'] / ['ios'] promoted: false, auth_required: false, icon: "📚", }; // Lazy-load the implementation. Static path, Turbopack resolves at build. export const load = () => import("./Dashlet");
Write the component
The component runs entirely in the browser. Mark it "use client" and default-export it. The platform mounts it inside the shared Dashlet frame - header, breadcrumb, author byline, category chip, pricing badge are all handled for you.
"use client"; import { useState } from "react"; import JSZip from "jszip"; export default function PdfMergerDashlet() { const[files, setFiles] = useState<File[]>([]); async function merge() { // your magic. Canvas, fetch, WASM, whatever. } return ( <div className="space-y-5"> /* your tool's UI */ </div> ); }
Register it
One import line and one push to the array in dashlets/_registry.ts. The registry validates every manifest with Zod at module load. Invalid shape, missing load, or duplicate slug fails the build with the offending name.
import * as ScreenshotResizer from "./screenshot-resizer/manifest"; import * as InstagramCrop from "./instagram-crop/manifest"; import * as ReceiptOcr from "./receipt-ocr/manifest"; import * as PdfMerger from "./pdf-merger/manifest"; // add this const RAW_MODULES = [ ScreenshotResizer, InstagramCrop, ReceiptOcr, PdfMerger, // add this ];
That's it
Run npm run dev. Your tool is now at /dashlet/pdf-merger, showing in the catalog, indexed by search, listed on your author page, and exposed in the manifest API. Nothing else to wire.
Every field in the manifest.
| Field | Notes |
|---|---|
| slug | URL segment. Lowercase, kebab-case. Becomes /dashlet/<slug>. Must be unique across the platform. |
| title | Display title in cards, page header, search results. |
| tagline | One-line elevator pitch. Shows under the title everywhere. |
| description | Longer marketing copy. Used for SEO meta description. |
| category | One of the five fixed categories: app-store, tax, images, birthdays, pdfs. |
| keywords | Search hints. Indexed by Fuse.js (weight 1.0) and fed to the LLM fallback verbatim. |
| status | "live" renders your component. "coming-soon" renders a placeholder with related-tool suggestions. |
| pricing | See the next section. Three kinds: free, one-time, subscription. |
| author | Handle pointing to an entry in authors.json (or, eventually, a user profile). Renders the by-line on your Dashlet page and the /by/<author> portfolio. |
| platforms | Where the Dashlet runs. Defaults to ["web"]. Web requires a Dashlet.tsx in this folder; iOS / Android require a registered native view in their respective app. The /api/dashlets endpoint accepts ?platform=ios so the iOS app sees only its supported tools. |
| promoted | Default false. Admin-controlled in production via the dashlet_overrides table - your code defines the default, the platform owns the live state. |
| promoted_until | When set, promotion expires automatically. Otherwise it's permanent until toggled off. |
| auth_required | If true, anonymous visitors see a sign-in CTA instead of your component. |
| icon | An emoji or short string. Renders as the Dashlet's visual mark in cards, the page header, search results. |
Categories.
Five fixed categories today. Pick the one your tool belongs to - your Dashlet shows up in that section of the catalog and the category card on the homepage.
app-storetaximagesbirthdayspdfsNeed a new category? It's a code change today - open an issue or submit a PR. We add categories deliberately, not on demand.
Web, iOS, Android. Or any combo.
The platforms field on your manifest declares where the Dashlet runs. Web implementations live in this repo; iOS and Android implementations live in their respective native apps and register against the same slug.
Web only
platforms: ["web"]The default. Dashlet.tsx renders at /dashlet/<slug>. Won't show in the iOS catalog.
Cross-platform
platforms: ["web", "ios"]Same Dashlet, two implementations. Web gets the React component; iOS gets a SwiftUI view registered for the slug.
iOS only
platforms: ["ios"]Native-only. Web shows a chip on the card and an 'open in iOS app' page if someone clicks through.
Dashlet.tsx file (return null is fine). The registry imports it but the page never mounts it. Keeps the contract uniform.Pricing options.
Three kinds. Set on the manifest as a discriminated union - the type system enforces the right shape per kind.
Free
No payment surface. Just runs.
pricing: {
kind: "free"
}One-time
Charge once. The user owns the unlock.
pricing: {
kind: "one-time",
price_cents: 199
}Subscription
Recurring. Monthly or yearly.
pricing: {
kind: "subscription",
price_cents: 500,
interval: "month"
}Eight surfaces. None of them your problem.
When your manifest registers, all of this happens automatically. You write the tool; the platform writes everything around it.
/dashlet/<slug>
A dedicated page with breadcrumb, header band, author byline, category chip, pricing badge, and your component mounted in the shared frame.
Catalog grid
Your card appears in the category section on the homepage and in the full catalog with the right hover state and pricing chip.
Category showcase
The category gradient card on the homepage counts your tool, and you appear in the “What's in here” preview list when you're in the featured category.
Fuzzy + LLM
Indexed by Fuse.js on title (×2), tagline (×1.5), keywords (×1), category (×0.5). When fuzzy returns nothing, an LLM picks up natural-language queries.
/by/<author>
Listed on your author profile alongside your other Dashlets, with the verified badge if applicable and tool count stats.
/api/dashlets
Your manifest is exposed in the JSON endpoint that the future iOS app consumes. Same metadata, two front-ends.
Meta tags
Title becomes the page title, description becomes the meta description, slug becomes a clean shareable URL. Everything Google needs.
Admin overrides
The platform admin can flip promoted/hidden via the admin panel without touching your code. Your manifest defines defaults; ops owns live state.
Recently used
When a user opens your Dashlet, it lands in their “Where you left off” strip on the dashboard. Stored in their browser, never on our servers.
What it is. What it isn't.
It is
- ·A default-exported React component, marked
"use client" - ·Free to use any client-side library it bundles
- ·Free to do anything in the browser: Canvas, WebAssembly, fetch, IndexedDB, Web Workers
- ·Lazy-loaded - never costs a byte until someone visits its page
- ·Mounted inside the shared frame - you don't render the page header, breadcrumb, or footer
It isn't
- ·A server component. Server bits are reserved for the platform.
- ·A way to mutate platform data. No reaching into the database or other Dashlets' storage.
- ·Allowed to read the user's session, payment status, or other tools' data. Hard isolation.
- ·Allowed to define its own routes. The Dashlet lives at
/dashlet/<slug>and that's it.
Best practices.
Run in the browser when you can
The Screenshot Resizer never uploads the image. Canvas crop, JSZip package, download. Privacy is a real selling point - say so in your tagline.
One job, well
A Dashlet is a small panel of actually useful. Three knobs, not thirty. If you find yourself adding tabs, you're building two Dashlets.
Reuse the platform's primitives
Use @/components/ui/button, @/components/ui/input, @/components/ui/card so your tool feels like the platform. Bring your own styles if you want - but the design system is there.
Make the empty state useful
Tell the user what to drop in, what file types you accept, what comes out. The Screenshot Resizer's dropzone copy is a good template.
Stays-on-your-device beats fast-and-private
If you can avoid sending data to a server, do. Faster than the round-trip, no privacy questions, no infra to maintain.
Today vs. tomorrow.
PR-only.
Self-serve isn't open yet. The contract above is exactly what an outside contributor would target - it just goes through a pull request to our repo right now. Reach out before you start a big one.
Self-serve portal.
Upload a manifest module, get a sandbox preview, submit for review, ship. Profile pages, payouts, analytics. Same contract - just no PR required. Join the list to be in the first batch.
You've read the whole contract.
That's the whole thing.
Two files, one register line. The platform handles the rest.