# Widget development
A widget is two pieces:
1. a Go **fetcher** in `apps/backend/internal/widget/` that declares config
fields and pulls live data, and
2. a React **view** in `apps/frontend/src/widgets/` that renders the payload
inside a tile.
Renderers own the tile chrome — widgets never touch layout, drag-drop, or
popovers.
## Backend fetcher
One file, registered in `init()`:
```go
package widget
type pihole struct{}
func init() { Register(pihole{}) }
func (pihole) Meta() Type {
return Type{
Type: "pihole",
Name: "Pi-hole",
Description: "Queries and block rate",
Fields: []Field{
{Key: "endpoint", Label: "Endpoint", Placeholder: "http://pi.hole", Required: true},
{Key: "token", Label: "API token", Secret: true},
},
}
}
func (pihole) Fetch(ctx context.Context, cfg json.RawMessage) (any, error) {
// cfg holds the item's config JSON. Never log it — it may hold keys.
}
```
- `Meta()` drives the add-widget dialog: `GET /api/widgets/types` returns
every registered type with its fields.
- `Fetch` is called by `GET /api/widgets/:id/data`; responses are cached ~30s
in-process. Return any JSON-marshalable value — it reaches the frontend as
`data`.
- Read config with the `cfgStr`/`cfgNum` helpers in the package.
- Fetchers needing the database (monitor, domain, system) live in
`internal/api` and register via `widget.Register` at router setup — the
`widget` package stays a leaf with no DB dependency.
## Frontend view
One component in `apps/frontend/src/widgets/`, one line in the registry at
`src/widgets/index.tsx`:
```tsx
export function PiholeWidget({ data, error }: { item: Item; data?: WidgetData; error?: string }) {
if (error) return