# @doany-ai/app-core

> What every Doany site shares: the SDK client and query cache, sign-in state and forms, base UI, theme, images and uploads

Latest version **0.1.0-alpha.0** (published 2026-09-24) · MIT license · 0 weekly downloads

## Install

```sh
npm install @doany-ai/app-core
pnpm add @doany-ai/app-core
yarn add @doany-ai/app-core
bun add @doany-ai/app-core
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.1.0-alpha.0 |
| Published | 2026-09-24 |
| First published | 2026-09-24 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 42 |
| Unpacked size | 325.2 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | kalvinrc9, loner233 |

## Links

- npm: https://www.npmjs.com/package/@doany-ai/app-core
- npm.io page: https://npm.io/package/@doany-ai/app-core

## Dependencies (42)

- [clsx](https://npm.io/package/clsx.md) ^2.1.1
- [cmdk](https://npm.io/package/cmdk.md) ^1.0.0
- [vaul](https://npm.io/package/vaul.md) ^1.1.2
- [sonner](https://npm.io/package/sonner.md) ^2.0.1
- [recharts](https://npm.io/package/recharts.md) ^2.15.4
- [input-otp](https://npm.io/package/input-otp.md) ^1.4.2
- [next-themes](https://npm.io/package/next-themes.md) ^0.4.4
- [lucide-react](https://npm.io/package/lucide-react.md) ^0.475.0
- [tailwind-merge](https://npm.io/package/tailwind-merge.md) ^3.0.2
- [react-hook-form](https://npm.io/package/react-hook-form.md) ^7.54.2
- [react-day-picker](https://npm.io/package/react-day-picker.md) ^8.10.1
- [tailwindcss-animate](https://npm.io/package/tailwindcss-animate.md) ^1.0.7
- [@radix-ui/react-slot](https://npm.io/package/@radix-ui/react-slot.md) ^1.1.2
- [@radix-ui/react-tabs](https://npm.io/package/@radix-ui/react-tabs.md) ^1.1.3
- [embla-carousel-react](https://npm.io/package/embla-carousel-react.md) ^8.5.2
- [@radix-ui/react-label](https://npm.io/package/@radix-ui/react-label.md) ^2.1.2
- [@radix-ui/react-toast](https://npm.io/package/@radix-ui/react-toast.md) ^1.2.2
- [@radix-ui/react-avatar](https://npm.io/package/@radix-ui/react-avatar.md) ^1.1.3
- [@radix-ui/react-dialog](https://npm.io/package/@radix-ui/react-dialog.md) ^1.1.6
- [@radix-ui/react-select](https://npm.io/package/@radix-ui/react-select.md) ^2.1.6
- [@radix-ui/react-slider](https://npm.io/package/@radix-ui/react-slider.md) ^1.2.3
- [@radix-ui/react-switch](https://npm.io/package/@radix-ui/react-switch.md) ^1.1.3
- [@radix-ui/react-toggle](https://npm.io/package/@radix-ui/react-toggle.md) ^1.1.2
- [react-resizable-panels](https://npm.io/package/react-resizable-panels.md) ^2.1.7
- [@radix-ui/react-menubar](https://npm.io/package/@radix-ui/react-menubar.md) ^1.1.6
- [@radix-ui/react-popover](https://npm.io/package/@radix-ui/react-popover.md) ^1.1.6
- [@radix-ui/react-tooltip](https://npm.io/package/@radix-ui/react-tooltip.md) ^1.1.8
- [@radix-ui/react-checkbox](https://npm.io/package/@radix-ui/react-checkbox.md) ^1.1.4
- [@radix-ui/react-progress](https://npm.io/package/@radix-ui/react-progress.md) ^1.1.2
- [class-variance-authority](https://npm.io/package/class-variance-authority.md) ^0.7.1
- [@radix-ui/react-accordion](https://npm.io/package/@radix-ui/react-accordion.md) ^1.2.3
- [@radix-ui/react-separator](https://npm.io/package/@radix-ui/react-separator.md) ^1.1.2
- [@radix-ui/react-hover-card](https://npm.io/package/@radix-ui/react-hover-card.md) ^1.1.6
- [@radix-ui/react-collapsible](https://npm.io/package/@radix-ui/react-collapsible.md) ^1.1.3
- [@radix-ui/react-radio-group](https://npm.io/package/@radix-ui/react-radio-group.md) ^1.2.3
- [@radix-ui/react-scroll-area](https://npm.io/package/@radix-ui/react-scroll-area.md) ^1.2.3
- [@radix-ui/react-alert-dialog](https://npm.io/package/@radix-ui/react-alert-dialog.md) ^1.1.6
- [@radix-ui/react-aspect-ratio](https://npm.io/package/@radix-ui/react-aspect-ratio.md) ^1.1.2
- [@radix-ui/react-context-menu](https://npm.io/package/@radix-ui/react-context-menu.md) ^2.2.6
- [@radix-ui/react-toggle-group](https://npm.io/package/@radix-ui/react-toggle-group.md) ^1.1.2
- [@radix-ui/react-dropdown-menu](https://npm.io/package/@radix-ui/react-dropdown-menu.md) ^2.1.6
- [@radix-ui/react-navigation-menu](https://npm.io/package/@radix-ui/react-navigation-menu.md) ^1.2.5

## Recent versions

- 0.1.0-alpha.0 (latest) — 2026-09-24
- 0.1.0-alpha.1 (alpha) — 2026-09-24

## README

# @doany-ai/app-core

What every Doany site shares, as a package instead of scaffold source: the SDK
client and query cache for the whole page, sign-in state and forms, base UI
components, the theme, and images and uploads.

It does not create an SDK client or import `@doany-ai/sdk`: the site creates
one client and hands it to `<DoanyProvider>`, and every package on the page uses
that one — one sign-in, one query cache. It needs a client from
`@doany-ai/sdk` 0.3.0-alpha.0 or later.

## Entries

| Import | What | Needs |
| --- | --- | --- |
| `@doany-ai/app-core` | `DoanyProvider`, `useDoany`, `createQueryClient`, `readAppParams`, `cn` | react, @tanstack/react-query |
| `@doany-ai/app-core/auth` | `AuthProvider`, `useAuth`, `AuthGate`; `LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `AuthLayout`; the hooks the forms are built on: `useLogin`, `useRegister`, `useForgotPassword`, `useResetPassword`; `defaultAuthLabels` | — |
| `@doany-ai/app-core/router` | `ProtectedRoute`, `ScrollToTop`, `LoginPage`, `RegisterPage`, `ForgotPasswordPage`, `ResetPasswordPage` | react-router-dom 6.4+ |
| `@doany-ai/app-core/account` | `useMyContact`, `useUpdateMyContact` | `AuthProvider` |
| `@doany-ai/app-core/media` | `Image` (media-worker resizing, private files), `FileUpload` | — |
| `@doany-ai/app-core/ui` | the base components (button, card, dialog, form, sidebar, toast…), also one by one: `@doany-ai/app-core/ui/button` | — |
| `@doany-ai/app-core/styles.css` | the theme variables (also as `theme.css`) | — |
| `@doany-ai/app-core/tailwind-preset` | the theme as a Tailwind 3 preset, with the page-wide base rules | tailwindcss 3 |

## Set up

```jsx
// main.jsx
import "@doany-ai/app-core/styles.css"; // the theme variables; the site's CSS comes after and wins
import "./index.css";
import { createClient } from "@doany-ai/sdk";
import { DoanyProvider, readAppParams } from "@doany-ai/app-core";
import { AuthGate, AuthProvider } from "@doany-ai/app-core/auth";
import { LoginPage, ProtectedRoute, RegisterPage } from "@doany-ai/app-core/router";
import { Toaster } from "@doany-ai/app-core/ui";

const params = readAppParams({ appId: import.meta.env.VITE_DOANY_APP_ID });
const doany = createClient({ appId: params.appId, token: params.token, serverUrl: "" });

root.render(
  <DoanyProvider client={doany}>
    <AuthProvider token={params.token}>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<LoginPage authenticatedHome="/" />} />
          <Route path="/register" element={<RegisterPage />} />
          <Route path="*" element={<AuthGate><SiteRoutes /></AuthGate>} />
        </Routes>
        <Toaster />
      </BrowserRouter>
    </AuthProvider>
  </DoanyProvider>
);
```

```js
// tailwind.config.cjs — one build for the site's classes and the packages'
module.exports = {
  presets: [require("@doany-ai/app-core/tailwind-preset")],
  content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}", "./node_modules/@doany-ai/*/dist/**/*.js"],
};
```

The packages ship no utilities: the site's Tailwind build scans them. Two
builds on one page break the components — each is ordered on its own, so a
utility the site also uses (`px-3`) lands after a component's more specific
one (`pl-10`, the login form's room for its icon) and overrides it. One build
orders them together.

## Customizing

Three levels, each without touching the package's code. Try them in this order.

**1. CSS only — the theme.** Set the variables on `:root`, after the package's
CSS. Every component of every Doany package follows, dialogs included. On
`:root`, not on a wrapper: dialogs and menus render at the end of `<body>`,
outside any wrapper.

```css
:root { --primary: 221 83% 53%; --radius: 1rem; --font-body: "Inter", sans-serif; }
```

The variables: `--background`, `--foreground`, `--card`, `--popover`,
`--primary`, `--secondary`, `--muted`, `--accent`, `--destructive`,
`--success` (paid, confirmed), `--border`, `--input`, `--ring` (focus), each
with its `-foreground` where text sits on it; `--radius`; `--font-body`,
`--font-heading`. The defaults are in `src/styles/theme.css`, with a `.dark`
set.

**2. Props only — words, parts, links, behavior.** The sign-in forms take
`title`, `subtitle`, `icon`, `providers` (`[]` hides Google), the other pages'
hrefs, `authenticatedHome`, `renderLink`, and `onSignedIn` / `onAccountCreated`
/ `onReset` in place of the default page load. Every word they show is in
`labels` — pass a part of `AuthLabels` and the rest stay the default; one
object can carry a whole translation to every form:

```jsx
<LoginForm title="Mori" providers={[]} labels={{ email: "Work email", logIn: "Sign in", loginFailed: "Check your email and password" }} />
```

A failure is said in the platform's words (too many attempts, a taken email)
unless the site sets its label (`loginFailed`, `registerFailed`, …): then
always in the site's — a translated site shows no English error.

`AuthGate` takes `loading` and `notRegistered`; `ProtectedRoute` takes
`fallback`, `unauthenticatedElement`, `notRegistered` and `loginPath`;
`UserNotRegisteredError` and `FileUpload` take `labels` too.

**3. Hooks only — your own markup.** The forms are markup over hooks that keep
their behavior: where a sign-in comes back to (same origin only, never a
sign-in page), how a new account gets its session, when the site learns who
signed in.

```jsx
const { login, loginWithProvider, pending, error } = useLogin({ onSignedIn: (to) => navigate(to) });
// error: { reason: "rejected" | "account_unavailable" | …, message } — say it in your words
```

`useRegister` (with its `verify` step), `useForgotPassword`,
`useResetPassword`, and `useAuth()` / `useDoany()` for the state and the
client.

## Where it came from

Extracted from the v5 scaffold's `src` (since removed; `doany-general-scaffold` is built on these packages instead), keeping its behavior:

| Here | Scaffold |
| --- | --- |
| `DoanyProvider`, `createQueryClient` | `App.jsx`'s `QueryClientProvider`, `lib/query-client.js`; the client came from `api/doanyClient.js` |
| `readAppParams` | `lib/app-params.js` (defaults passed in instead of read from `import.meta.env`) |
| `auth/` | `lib/AuthContext.jsx`, `lib/login-code.js`, `lib/login-navigation.js`, `pages/*`, `components/AuthLayout.jsx`, `GoogleIcon.jsx`, `UserNotRegisteredError.jsx`, `App.jsx`'s auth gate |
| `router/` | `components/ProtectedRoute.jsx`, `components/ScrollToTop.jsx` |
| `media/` | `components/ui/image.jsx`, `file-upload.jsx`, `lib/image-layout.js`, `hooks/use-size.jsx` |
| `ui/` | `components/ui/*`, `hooks/use-mobile.jsx` |

`AuthProvider` reads the site's settings with `doany.app.getPublicSettings()`
and trades a sign-in code with `doany.auth.exchangeLoginCode()` instead of
building its own axios clients from SDK internals.

The `ui/` components are shadcn's new-york TypeScript sources with the
scaffold's differences applied, so each file, with its types stripped, matches
the scaffold's `.jsx` — class strings, `TOAST_LIMIT = 20`, `SidebarTrigger`'s
`asChild`. Kept from upstream where it is only safer: `FormMessage` and
`ChartTooltipContent` guard an undefined value, and the mobile sidebar carries a
screen-reader title.

## Develop

```bash
npm ci
npm test          # vitest + jsdom
npm run typecheck
npm run build     # dist/: ESM + .d.ts per module, styles.css (the theme variables)
```

---
_Source: https://npm.io/package/@doany-ai/app-core · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
