tiredof.app
← For developers
Build a Dashlet

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.

dashlets/<your-slug>/
📄
manifest.ts
Metadata - slug, title, pricing, category. Validated at build.
⚛︎
Dashlet.tsx
Default-exported React component. Runs in the browser.
+
One `import * as` line in _registry.tsand you're live.
Mental model

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.

manifest.ts

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.

Dashlet.tsx

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.

The contract

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.

01

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.

BASH
mkdir dashlets/pdf-merger
02

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.

dashlets/pdf-merger/manifest.ts
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");
03

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.

dashlets/pdf-merger/Dashlet.tsx
"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>
  );
}
04

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.

dashlets/_registry.ts
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
];
05

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.

Reference

Every field in the manifest.

FieldNotes
slugURL segment. Lowercase, kebab-case. Becomes /dashlet/<slug>. Must be unique across the platform.
titleDisplay title in cards, page header, search results.
taglineOne-line elevator pitch. Shows under the title everywhere.
descriptionLonger marketing copy. Used for SEO meta description.
categoryOne of the five fixed categories: app-store, tax, images, birthdays, pdfs.
keywordsSearch 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.
pricingSee the next section. Three kinds: free, one-time, subscription.
authorHandle 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.
platformsWhere 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.
promotedDefault false. Admin-controlled in production via the dashlet_overrides table - your code defines the default, the platform owns the live state.
promoted_untilWhen set, promotion expires automatically. Otherwise it's permanent until toggled off.
auth_requiredIf true, anonymous visitors see a sign-in CTA instead of your component.
iconAn emoji or short string. Renders as the Dashlet's visual mark in cards, the page header, search results.
Pick a home

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-store
App Store
tax
Tax
images
Images
birthdays
Birthdays
pdfs
PDFs

Need a new category? It's a code change today - open an issue or submit a PR. We add categories deliberately, not on demand.

Platforms

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.

Pro tip: if you ship a non-web Dashlet, you still need the Dashlet.tsx file (return null is fine). The registry imports it but the page never mounts it. Keeps the contract uniform.
Money

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"
}
Today:the checkout flow is scaffolded but not wired. Paid Dashlets show a placeholder paywall. When checkout ships, your code doesn't change - the platform just starts respecting your pricing field.
What ships with every Dashlet

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.

Page

/dashlet/<slug>

A dedicated page with breadcrumb, header band, author byline, category chip, pricing badge, and your component mounted in the shared frame.

Discovery

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.

Discovery

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.

Search

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.

Profile

/by/<author>

Listed on your author profile alongside your other Dashlets, with the verified badge if applicable and tool count stats.

API

/api/dashlets

Your manifest is exposed in the JSON endpoint that the future iOS app consumes. Same metadata, two front-ends.

SEO

Meta tags

Title becomes the page title, description becomes the meta description, slug becomes a clean shareable URL. Everything Google needs.

Ops

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.

Tracking

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.

Your component

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.
How the good ones look

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.

How you publish

Today vs. tomorrow.

Today

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.

Soon

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.