useAppAlertuseAppConfiguseAxiosuseBroadcastChanneluseBroadcastSubscriptionuseDatastoreMutationsuseFetchDatastoreuseFetchFileuseFetchPortfoliouseFetchWorkflowExecutionsuseFetchWorkflowsuseFetchWorkspacesuseFileMutationsusePortfolioMutationsuseRunWorkflowMutations
AppAlertProviderAppConfigProviderBroadcastChannelProviderBroadcastChannelHandler
This README summarizes the project layout and how to use the React hooks, contexts and the Vite setup included in this repository.
- React 19 + TypeScript app scaffold with MUI 7 and Tailwind CSS 4.
- TanStack Query v5 used across all data-fetching and mutation hooks.
- Context providers for app config, global alerts, and cross-tab messaging.
EVERYSK_APP_NAMEauto-injected into<title>at dev and build time via Vite plugin.- Dev proxy (
/api) forwards to the Everysk API in development; same path works in production.
src/App.tsx,main.tsx— application entry and root component.components/themeProviderWrapper/— theme wrapper available to the app.ui/everyskIcon— small UI pieces.
contexts/appConfigContext— AppConfig provider + types.appAlertContext— AppAlert provider + types.broadcastChannelContext— Broadcast provider + helpers.
hooks/— main integration surface (see “Hooks” section below).pages/— route pages (home, index).utils/— API clients, query client setup and helpers.
dev/app-config.dev.json— development app config served by a Vite plugin.
vite/serverProxy.ts— dev server proxy configuration.plugins/envVarsLocation.tsserveDevAppConfig.ts— servesdev/app-config.dev.jsonduring development.
vite.config.ts— registers plugins and the dev server proxy.
These are never committed to the repository.
| Variable | Required | Description |
|---|---|---|
EVERYSK_API_SID |
Yes | Your Everysk API account SID |
EVERYSK_API_TOKEN |
Yes | Your Everysk API authentication token |
EVERYSK_APP_NAME |
Yes | Your Everysk application name (used during deploy) |
ANTHROPIC_API_KEY |
No | Your Anthropic API key for Claude AI features |
Get your Everysk credentials from your Everysk account dashboard. Get your Anthropic API key from console.anthropic.com.
These are set in .replit [userenv.shared] and do not need to be added as secrets.
| Variable | Value | Description |
|---|---|---|
PORT |
5000 |
Dev server port |
EVERYSK_API_URL |
https://api.everysk.com/v2 |
Everysk API base URL |
EVERYSK_MANAGED_DEPLOY |
true |
Enables managed deploy mode |
When you import this template into Replit:
- Click the Secrets tab (lock icon) in the left sidebar
- Add secret:
EVERYSK_API_SID→ paste your API SID - Add secret:
EVERYSK_API_TOKEN→ paste your API token - Add secret:
EVERYSK_APP_NAME→ paste your Everysk application name - (Optional) Add secret:
ANTHROPIC_API_KEY→ paste your Anthropic API key (required only for Claude AI features) - Click Run — the dev server starts on port 5000. This does not deploy — to deploy, click "Deploy App" in the Workflows tab after setup is complete.
The app validates these secrets on startup. If they are missing, you will see instructions in the console.
To enable automated deployment via .github/workflows/deploy.yaml:
- Go to your repository Settings → Secrets and variables → Actions
- Add repository secrets:
EVERYSK_API_SID→ your API SIDEVERYSK_API_TOKEN→ your API token
- (Optional) Add repository variable:
EVERYSK_API_URL→ custom API endpoint (defaults tohttps://api.everysk.com/v2)
The workflow triggers via manual dispatch (Actions tab → Deploy App → Run workflow).
-
Install dependencies
npm install
-
Start dev server
bash scripts/check-env.sh && npm run devDefault port:
5000(set viaPORTenv var; pre-configured by Replit). -
Open the app
"$BROWSER" http://localhost:5000
- Build:
npm run build
- Preview:
npm run preview
The file dev/app-config.dev.json configures the frontend for local development and is also included in production deploys.
{
"app": "your-everysk-app-name"
}How it works:
- In development (
npm run dev): Vite serves this file at/app-config.dev.json;src/main.tsxfetches it on startup and merges it intowindow.APP_CONFIG. - In production (deploy):
scripts/deploy.pyreads this file and sends it asenv=in the deploy POST to the Everysk API, which stores and injects it at runtime.
What to set:
"app"— set to the same value as yourEVERYSK_APP_NAMEsecret. This populateswindow.APP_CONFIG.appin the running app.
Title injection: The browser <title> is set from EVERYSK_APP_NAME automatically by the Vite plugin at both dev and build time — you do not need to update index.html manually.
| Workflow | Trigger | Command | Description |
|---|---|---|---|
| Project (Run button) | Auto on import + Run button | bash scripts/check-env.sh && npm run dev |
Validates secrets, then starts the dev server on port 5000 |
| Deploy App | Manual only — never auto-run | bash scripts/replit-deploy.sh |
Builds the frontend, packages dist/, and deploys to the Everysk API |
⚠️ Deploy App is manual only. Never runscripts/replit-deploy.shautomatically on import, first run, or initial setup. Click "Deploy App" in the Replit Workflows tab only when you intend to deploy to production.
The "Deploy App" workflow requires EVERYSK_API_SID, EVERYSK_API_TOKEN, and EVERYSK_APP_NAME to be set in Secrets. On first deploy, EVERYSK_APP_NAME is written into config.json and subsequent deploys use the stored name. The app settings from dev/app-config.dev.json are automatically included as env in the deploy payload.
- Provider: Anthropic directly (user's own API key)
- SDK:
@anthropic-ai/sdk - Secret:
ANTHROPIC_API_KEY(add to Replit Secrets — optional, only needed for Claude features) - Available models:
claude-opus-4-6,claude-sonnet-4-6,claude-haiku-4-5 - Reference files:
.replit_integration_files/— template code for chat routes, batch processing, and storage patterns
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const message = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }],
});- The project root is used as
envDir(reads.envfrom project root). - Dev-only config is served from
/app-config.dev.jsonbyserveDevAppConfigPlugin(serve mode only). EVERYSK_APP_NAMEis auto-injected into<title>byenvVarsLocationPluginin both dev and build.- In build mode,
envVarsLocationPluginalso injects<meta name="app-config">intoindex.html. - The dev proxy (
/api→ Everysk API) is enabled only in serve mode (npm run dev).
The frontend uses /api as the base path for API calls.
- In development (
npm run dev), Vite forwards/apirequests via a dev proxy to the Everysk API. - In production,
/apiis resolved by the hosting environment (server/gateway) to the Everysk API.
This keeps client code identical across environments—no manual base URL configuration is required.
All providers are wired in src/App.tsx. The required nesting order (outermost first):
import { queryClient } from "./utils/queryClient";
import { BroadcastChannelProvider } from "./contexts/broadcastChannelContext";
import { AppConfigProvider } from "./contexts/appConfigContext";
import { ThemeProviderWrapper } from "./components/themeProviderWrapper";
import { QueryClientProvider } from "@tanstack/react-query";
import { AppAlertProvider } from "./contexts/appAlertContext";
function App() {
return (
<BroadcastChannelProvider>
<AppConfigProvider>
<ThemeProviderWrapper>
<QueryClientProvider client={queryClient}>
<AppAlertProvider>
{/* your page content here */}
</AppAlertProvider>
</QueryClientProvider>
</ThemeProviderWrapper>
</AppConfigProvider>
</BroadcastChannelProvider>
);
}Why this order:
BroadcastChannelProvideroutermost — no dependencies, provides cross-tab messaging to everythingAppConfigProvider— readswindow.APP_CONFIG(available immediately after bootstrap)ThemeProviderWrapper— MUI theme must wrap Query and Alert (Alert uses MUI components)QueryClientProvider— must wrap any component that callsuseQuery/useMutationAppAlertProvider— innermost; hooks that show alerts live inside Query context
Global alert/snackbar provider (MUI Snackbar + Alert), exposed via useAppAlert.
What it does
- Keeps an internal alert state (
open,message,severity,autoHideDuration,anchorOrigin). - Exposes an imperative API:
showAlert(options)opens/updates the alert (shallow merge +open: true)hideAlert()closes the alert (open: false)
- Ignores
"clickaway"close events to prevent accidental dismiss.
Exposes runtime configuration to the app via useAppConfig.
Config sources
window.APP_CONFIG: base config injected at runtime (e.g., by the server)./app-config.dev.json(DEV only): optional overrides fetched during bootstrap.
Merge strategy
- Shallow merge at bootstrap:
window.APP_CONFIG = { ...window.APP_CONFIG, ...devEnvConfig }
- Nested objects are replaced (no deep merge).
Exposed values
appId: fromwindow.APP_ID(ornull)appEnvironmentVar: the final merged config object
Thin wrapper around the browser BroadcastChannel API that provides a small pub/sub layer.
What it does
- Opens a channel by name
- Allows listeners to subscribe/unsubscribe
- Forwards each incoming message payload to all listeners
- Supports sending messages and closing the channel
Example
const handler = new BroadcastChannelHandler("my-channel");
const unsubscribe = handler.listen((msg) => {
console.log("received:", msg);
});
handler.sendMessage({ type: "PING", payload: { at: Date.now() } });
unsubscribe();
handler.close();React provider that exposes a lightweight cross-tab pub/sub API (backed by BroadcastChannelHandler).
Exposed API
post(message)publishes a messagesubscribe(fn)registers a listener and returns an unsubscribe functionlastMessage(optional convenience) keeps the last received message
Important behavior
- Subscriptions can be created before the handler exists (provider creates handler in
useEffect). - To avoid missing early subscriptions, callbacks are buffered and flushed once the handler is ready.
- For high-throughput message streams, note that
lastMessageupdates will re-render consumers that read it.
Convenience hook to access the global alert API from AppAlertProvider.
Returns
showAlert(options)to display/update a toasthideAlert()to close it
Usage
const { showAlert } = useAppAlert();
showAlert({
message: "Saved successfully.",
severity: "success",
autoHideDuration: 3000,
});Convenience hook to access runtime config from AppConfigProvider.
Returns
appIdappEnvironmentVar(final merged config)
Usage
const { appId, appEnvironmentVar } = useAppConfig();
console.log(appId, appEnvironmentVar);Returns a memoized Axios client already configured to call the Everysk API.
Recommended usage
- You do not need (and should not) pass any
urltouseAxios. - In both development and production, the app routes requests correctly to the Everysk API using the same default base path.
How it works
- By default,
useAxiosuses/apias thebaseURL. - In development, requests to
/apiare forwarded by the Vite dev proxy. - In production,
/apiis resolved by the deployment environment (server/gateway) to the Everysk API.
Notes
- The hook adds a request interceptor for
GET/DELETEthat attempts to parseconfig.params.query(JSON string), extract aworkspacefilter (if present), and inject it intoconfig.params.workspace.
Returns
- An object containing the configured axios instance (typically
{ api })
Usage
import { useAxios } from "./hooks/useAxios";
const MyComponent = () => {
const { api } = useAxios();
useEffect(() => {
api.get("/some/endpoint").then((res) => console.log(res.data));
}, [api]);
return null;
};Convenience hook to access the broadcast channel API from BroadcastChannelProvider.
Returns (typical)
post(message)to publishsubscribe(fn)to listen (returns unsubscribe)lastMessagesnapshot (if enabled by provider)
Usage
const { subscribe, post } = useBroadcastChannel();
useEffect(() => {
const unsub = subscribe((msg) => console.log(msg));
return unsub;
}, [subscribe]);
post({ type: "PING", payload: { at: Date.now() } });Higher-level helper that subscribes on mount and unsubscribes on unmount.
When to use
- You want the “correct pattern” baked in (effect + cleanup).
- You want to avoid accidental multiple subscriptions due to re-renders.
Usage
useBroadcastSubscription((msg) => {
if (msg.type === "PING") {
console.log("ping:", msg.payload);
}
});Data fetching hook (TanStack Query) to retrieve one datastore (id provided) or a list (id: null).
Returns
- Standard TanStack Query result (
data,isLoading,isFetching,error,refetch, etc.) queryKey: stable key for cache invalidation reusedatastoresProps: metadata map keyed by datastore id (useful for UI)
Data shape (important)
idprovided →datais an array of row objects for that datastoreid: null+mergeResult: false→datais an array of arrays (grouped per datastore)id: null+mergeResult: true→datais a single flat array- Each row includes
datastoreIdso you can trace the source datastore
Example
import { useFetchDatastore } from "./hooks/useFetchDatastore";
import type { FilterClause } from "./types/entityQuery";
export function DatastoreResultsExample() {
const filters: FilterClause[] = [
{ field: "workspace", value: "workspace" },
{ field: "link_uid", value: "linkuid" },
{ field: "date", op: ">=", value: "20260110" },
{ field: "date", op: "<=", value: "20260114" },
];
const {
data,
datastoresProps,
isLoading,
isFetching,
error,
refetch,
} = useFetchDatastore({
id: null,
filters,
mergeResult: true,
order: ["date desc"],
queryOptions: {
staleTime: 30_000,
refetchOnWindowFocus: false,
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error</div>;
return (
<div>
{isFetching && <div>Refreshing...</div>}
<button onClick={() => refetch()}>Refetch</button>
<h3>Datastores metadata</h3>
<pre>{JSON.stringify(datastoresProps, null, 2)}</pre>
<h3>Rows (merged)</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}Fetches a file (when id is provided) or a list of files (when id: null).
Returns
- Standard TanStack Query result
queryKeyfor reuse in invalidation
Notes
- Always returns an array: for single fetch, it’s a single-item array.
- File content is carried in
File.dataand is expected to be Base64 when present.
Example
const { data, isLoading } = useFetchFile({
id: "file-123",
filters: [{ field: "workspace", value: "ws-1" }],
});
// data -> File[] (single-item array)Fetches a portfolio (when id is provided) or a list of portfolios (when id: null).
Returns
- Standard TanStack Query result
queryKeyfor reuse in invalidation
Notes
- Always returns an array: for single fetch, it’s a single-item array.
Example
const { data, isLoading } = useFetchPortfolio({
id: "pf-123",
filters: [{ field: "workspace", value: "ws-1" }],
});
// data -> Portfolio[] (single-item array)Mutation hook (TanStack Query) for datastore write operations: create / update / delete.
Returns
- An object containing three mutation handlers:
create,update,remove - Each handler is a normal TanStack mutation result (supports
mutate,mutateAsync,isPending, etc.)
Behavior
- Uses
useAppAlertfor success/error messages. - If you pass a
queryKey, it invalidates cache after successful mutations. removerequiresworkspace(API requires it as query param).
Example
const { create, update, remove } = useDatastoreMutations({ queryKey: ["datastore"] });
create.mutate({ data: { name: "My datastore", workspace: "ws-1" } });
update.mutate({ id: "ds-123", data: { name: "Renamed datastore" } });
remove.mutate({ id: "ds-123", workspace: "ws-1" });Fetches a list of workflows, optionally filtered by workspace.
Returns
- Standard TanStack Query result (
data,isLoading,isFetching,error,refetch, etc.) queryKeyfor cache invalidation reusedatais aWorkflow[]array
Example
const { data: workflows, isLoading } = useFetchWorkflows({
workspace: "ws-1",
staleTime: 30_000,
});Fetches executions for one or more workflows in parallel (one query per workflow ID using useQueries).
Returns
data: flatWorkflowExecution[]merged across all queried workflow IDsisLoading: true if any query is loadingisFetching: true if any query is fetchingrefetch(): triggers refetch on all queriesqueries: raw array of individual query results
Example
const { data: executions, isLoading } = useFetchWorkflowExecutions({
workflowIds: ["wf-123", "wf-456"],
refetchInterval: 5000,
});Fetches all workspaces. Refetches on every mount (refetchOnMount: "always").
Returns
- Standard TanStack Query result
queryKeyfor cache invalidation reusedatais aWorkspace[]array
Example
const { data: workspaces, isLoading } = useFetchWorkspaces();Mutation hook (TanStack Query) for file write operations: create / update / delete.
Returns
{ create, update, remove }mutation handlers (TanStack standard)
Behavior / notes
- Uses
useAppAlertfor success/error messages. - Optional cache invalidation via
queryKey. - File content must be Base64 in
data(raw Base64 only; nodata:<mime>;base64,prefix). removerequiresworkspace.
Example
const fetch = useFetchFile({
id: null,
filters: [{ field: "workspace", value: "ws-1" }],
});
const { create } = useFileMutations({ queryKey: fetch.queryKey });
await create.mutateAsync({
data: {
name: "My file",
workspace: "ws-1",
content_type: "text/plain",
version: "1",
data: "SGVsbG8gd29ybGQ=",
},
});Mutation hook (TanStack Query) for portfolio write operations: create / update / delete.
Returns
{ create, update, remove }mutation handlers (TanStack standard)
Behavior
- Uses
useAppAlertfor success/error messages. - Optional cache invalidation via
queryKey. removerequiresworkspace.
Example
const fetch = useFetchPortfolio({
id: null,
filters: [{ field: "workspace", value: "ws-1" }],
});
const { update } = usePortfolioMutations({ queryKey: fetch.queryKey });
update.mutate({
id: "pf-123",
data: { name: "Renamed Portfolio" },
});Mutation hook to execute workflows (async or sync).
Returns
runAsync: starts an execution (non-blocking)runSync: runs and returns the response immediately (preferred when you need output right away)
Behavior
- Uses
useAppAlertfor success/error messages. - Designed as mutations (workflow execution is a side-effect, not a query).
Example
const { runSync } = useRunWorkflowMutations();
const result = await runSync.mutateAsync({
id: "wf-123",
workspace: "ws-1",
parameters: { UID: "ABC123" },
});
console.log(result);