# Streams
> Streams is an agentic-first Laravel package system (Tailwind, Alpine, Laravel, Livewire) for data modeling, admin UI, REST APIs, and developer tooling. Streams are defined as JSON in streams/*.json and queried through a criteria and repository abstraction, so storage can change without changing application code.
---
# Get started: Introduction
> Streams overview for Laravel developers and teams.
Source: https://streams.dev/docs/introduction
## What is Streams?
Streams is a system of unified Laravel packages that provide an optimized foundation for **data modeling**, **admin interfaces**, **APIs**, and **development workflow**.
Your team defines domain data as JSON stream configurations in `streams/`. Those definitions are version-controlled, reviewable, and shared across environments—the same way you already manage Laravel config and migrations.
> Streams leans on domain-driven design. We call these domain abstractions **streams**.
Working on **this repository**? Start with [This project](/docs/this-project).
## Who it is for
Streams is built for **Laravel developers and teams** shipping real products:
- Custom CMS and content sites
- SaaS backends and tenant admin
- Internal admin and product control panels
- Headless APIs for mobile or SPA clients
- Starter projects and composable addons
You can adopt Core alone or compose **Core + UI + API + SDK + Testing** as your project requires.
## Core packages
| Package | Role |
|---------|------|
| [Streams Core](/docs/core/introduction) | Data modeling, repositories, field types |
| [Streams UI](/docs/ui/introduction) | Control panels, forms, tables, pages |
| [Streams API](/docs/api/introduction) | REST endpoints for stream data |
| [Streams SDK](/docs/sdk/introduction) | Scaffolding and dev workflow |
| [Streams Testing](/docs/testing/introduction) | Test environment and helpers |
## Next steps
- [Installation](/docs/installation) — new or existing Laravel projects
- [Architecture](/docs/architecture) — how the pieces fit together
- [Use cases](/docs/use-cases) — pick a path for your product type
- [Configuration](/docs/configuration) — Laravel config and `streams/` layout
## Community
- [Discord](https://discord.gg/vhz8NZC)
- [GitHub](https://github.com/laravel-streams/streams)
- [Stack Overflow](https://stackoverflow.com/search?q=laravel+streams)
---
# Get started: Installation
> Install Streams in a new or existing Laravel app: requirements, the version matrix, Composer constraints, and publishing config.
Source: https://streams.dev/docs/installation
## Server requirements
Streams requires a standard [Laravel-compatible environment](https://laravel.com/docs/deployment#server-requirements) with **PHP 8.2 or newer**. Core supports Laravel 10, 11, and 12, and this site (streams.dev) runs Laravel 12. The starter project and the `streams/testing` harness are still Laravel 10 only, and the SDK's MCP server needs Laravel 11.45+ or 12.41+. The [version matrix](#version-matrix) below has the details.
For image handling, install GD or the Imagick PHP extension. See [Images](/docs/images) and [Core images](/docs/core/images).
## Version matrix
| Package | Laravel 10 | Laravel 11 | Laravel 12 | Notes |
|---------|:----------:|:----------:|:----------:|-------|
| `streams/core` | Yes | Yes | Yes | Declares `^10\|^11\|^12`. Its CI runs on Laravel 10, through `streams/testing`. |
| `streams/ui` | Yes | Yes | Yes | Follows Core. Requires Livewire 3. |
| `streams/api` | Yes | Yes | Yes | Follows Core. |
| `streams/sdk` generators and `streams:validate` | Yes | Yes | Yes | Its own test suite runs on Laravel 12. |
| `streams/sdk` MCP server | No | 11.45+ | 12.41+ | Needs `laravel/mcp ^1.0`. On Laravel 10 it is skipped; use `streams:list --json` and `streams:validate --json`. |
| `streams/testing` | Yes | Not yet | Not yet | Requires `orchestra/testbench ^8.36` (Laravel 10 only). Being widened to `^8.36\|^9.15\|^10.8` for Laravel 11 and 12. |
| `streams/streams` starter | Yes | No | No | Pins `laravel/framework ^10.0`. |
| streams.dev (this site) | No | No | Yes | Runs Laravel 12 on PHP `^8.2`, with Core (`rc/prep`), UI, and the SDK as a dev dependency. |
Every PHP package needs **PHP 8.2 or newer**: Core, API, and SDK declare `php: ^8.2`, and UI and Testing require Core. Until `streams/testing` supports Laravel 11 and 12, the package test suites for Core, UI, and API run on Laravel 10 only, so treat 11 and 12 as supported but not yet verified in CI. See [Versions and support](/docs/versions) for install constraints and branches.
## New projects
The fastest path is the official Streams starter:
```bash
composer create-project streams/streams:1.0.x-dev my-app
cd my-app
php artisan serve
```
The starter ([laravel-streams/streams](https://github.com/laravel-streams/streams)) is a Laravel 10 application that requires Core, UI, and API. It has no tagged release yet, so the version is the `1.0` development branch.
## This repository (streams.dev)
**streams.dev** is not the generic starter — it is the documentation site. Clone it to work on docs or reference patterns:
```bash
git clone git@github.com:laravel-streams/streams.dev.git
cd streams.dev
composer install
npm install && npm run dev
php artisan serve
```
Production dependencies in this repo:
- [streams/core](/docs/core/introduction)
- [streams/ui](/docs/ui/introduction)
Dev dependency:
- [streams/sdk](/docs/sdk/introduction)
**streams/api** is not required here. Add it when you need REST endpoints (see [API installation](/docs/api/installation)).
See [This project](/docs/this-project) and [Local development](/docs/local-development).
## Existing Laravel projects
Add only the packages you need:
```bash
composer require streams/core:2.0.x-dev
composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev
composer require streams/core:2.0.x-dev streams/api:1.0.x-dev
```
Core is the only required package. UI and API are optional layers.
Use the explicit `x-dev` constraints. None of these packages has a stable 2.x (Core) or 1.x (UI, API) tag yet, and a bare `composer require streams/core` installs the old **Core 1.10.4**. Alternatively, set `"minimum-stability": "dev"` and `"prefer-stable": true` in your `composer.json`. See [Versions and support](/docs/versions).
### Publish and configure
Publishing is optional; Core works with its defaults. To change the config, publish it from Core's provider:
```bash
php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=config
```
Core registers three tags: `config` (`config/streams/core.php`), `streams` (Core's own stream definitions, copied into `streams/`), and `public` (Core's public assets, into `public/vendor/streams/core`). UI and API also use the `config` tag, so pass `--provider` to publish only one package's file. See [Core installation](/docs/core/installation) and [Configuration](/docs/configuration).
### Team workflow
Commit `streams/` and `streams/data/` to version control so stream definitions and filebase content stay in sync across developers and environments. Keep secrets in `.env` only.
## Local package development
To contribute to Streams packages, use Composer path repositories pointing at local clones (see [Addons](/docs/addons) and [Project structure](/docs/project-structure)).
## Updating
Update individual packages:
```bash
composer update streams/core --with-dependencies
composer update streams/ui --with-dependencies
composer update streams/api --with-dependencies
```
Or update the full project:
```bash
composer update
```
## Related
- [Versions and support](/docs/versions)
- [Configuration](/docs/configuration)
- [Architecture](/docs/architecture)
- [Use cases](/docs/use-cases)
---
# Get started: Use Cases
> Starting paths for common product types with Streams.
Source: https://streams.dev/docs/use-cases
Pick the path closest to what your team is building. Each path lists packages to require and the first stream to define.
## Custom CMS or content site
**Packages:** `streams/core`, `streams/ui`
**First stream:** `pages` with filebase HTML or markdown entries and a `{path}` route.
Your team edits content as flat files or entries under `streams/data/`. UI provides an editorial control panel when you need one.
- [Content](/docs/content)
- [Routing](/docs/routing)
- [UI panels](/docs/ui/panels)
## SaaS product backend
**Packages:** `streams/core`, `streams/ui`, `streams/api`
**First stream:** a tenant-scoped domain model (e.g. `projects`, `subscriptions`) with Eloquent source.
Use Laravel auth and policies as usual. Expose a JSON API for your SPA or mobile clients; use UI for internal admin or customer account settings.
- [Users](/docs/users)
- [API introduction](/docs/api/introduction)
- [Databases](/docs/databases)
## Admin panel (internal ops)
**Packages:** `streams/core`, `streams/ui`
**First stream:** the primary entity your ops team manages (orders, users, inventory).
Define a panel in UI configuration with navigation, tables, and forms generated from stream fields.
- [Control panel](/docs/control-panel)
- [Forms](/docs/forms)
- [Tables](/docs/ui/tables)
## Product settings panel (customer-facing)
**Packages:** `streams/core`, `streams/ui`
**First stream:** settings or preferences owned by the authenticated user.
Smaller scope than a full admin CP—focused pages and wizards for your product's configuration surface.
- [UI pages](/docs/ui/pages)
- [Components](/docs/components)
## Headless API only
**Packages:** `streams/core`, `streams/api`
**First stream:** the resource you expose publicly or to partners.
Skip UI unless you later add an admin. Configure API interfaces, auth, and custom endpoints as needed.
- [API introduction](/docs/api/introduction)
- [Query parameters](/docs/api/query-parameters)
- [Client library](/docs/client/introduction)
## Greenfield starter
**Packages:** full starter via `composer create-project` (Core, UI, API, SDK)
Use the starter to learn conventions, then strip packages you do not need.
- [Installation](/docs/installation)
- [Addons](/docs/addons)
---
# Get started: Configuration
> Laravel config, environment variables, and the streams directory.
Source: https://streams.dev/docs/configuration
## Introduction
Streams uses Laravel config files and environment variables alongside stream definitions in `streams/`.
### Streams directory
Your team typically commits:
```files
├── streams/
│ ├── users.json
│ └── pages.json
├── streams/data/
│ └── … entry files …
```
Stream JSON describes domain models; `streams/data/` holds entries when using filebase storage.
### Configuration files
Published configuration files reside in `config/streams/`.
``` files
├── config/streams/
│ ├── core.php
│ ├── api.php
│ └── ui.php
```
### Publishing Configuration
Core, UI, and API all register a `config` tag, so this publishes every installed package's config file:
```bash
php artisan vendor:publish --tag=config
```
To publish configuration for a specific package, name its provider:
```bash
php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=config
```
The above commands will copy configuration files from their package location to the directory mentioned above so that you can modify them directly and commit them to your version control system.
## Environment Variables
It is often helpful to have different configuration values based on the environment in which your application is running. For example, you may wish to enable "debug mode" on your local server but not your production server.
### The `.env` File
Environmental variables are defined in the `.env` file in your project's root directory. In fresh installations, Composer will automatically rename the included `.env.example` file to `.env` for you.
You can manually copy and rename, or use `php -r "copy('.env.example', '.env');"` if the file does not already exist.
### Environment Variable Types
Variables in your `.env` files parse as strings. A couple specific values are worth noting:
```bash
EXAMPLE_VAR= # (string) ''
EXAMPLE_VAR=null # (null) null
```
If you need to define an environment variable value containing a space, you may enclose the value in double-quotes.
``` env
APP_NAME="Spaghetti + Meatballs"
```
### Retrieving Environment Variables
All environmental variables are available in configuration files by using the `env()` helper function. An optional second argument allows you to pass a default value.
``` php
// config/app.php
'debug' => env('APP_DEBUG', false),
```
Once passed into a config file, the variable is available using the `config()` helper function. Again, an optional second argument allows you to specify a default value.
``` php
// Retrieve the above 'debug' value:
config('app.debug', false)
```
### Do not version your `.env` file
The `.env` file **should not be committed to version control**. Each developer or server running your application may require a different environment configuration. It is also a security risk if a nefarious character gains access to your version control repository because sensitive data like credentials, API keys, and other configuration would be visible to them.
### Hiding Environment Variables from Debug Pages
When an exception is uncaught, and the `APP_DEBUG` environment variable is `true`, the debug page will show all environment variables and their assigned values. You may obscure variables by updating the `debug_blacklist` option in your `config/app.php` file.
``` php
return [
// ...
'debug_blacklist' => [
'_ENV' => [
'APP_KEY',
'SECRET_API_KEY',
'BITCOIN_WALLET_PW',
],
'_SERVER' => [
'APP_KEY',
'DB_PASSWORD',
],
'_POST' => [
'password',
],
],
];
```
Learn more about [environment configuration](https://laravel.com/docs/configuration#environment-configuration) in the Laravel docs.
---
# Get started: Versions and support
> Which Laravel and PHP versions each Streams package supports, and how to require them.
Source: https://streams.dev/docs/versions
Streams is pre-release. Most packages have **no tagged releases yet**, so you install them from their development branches. This page lists what each package declares in its `composer.json` (or `package.json`) and what it is tested against today.
## Support matrix
| Package | Install constraint | Branch | Laravel | PHP | Notes |
|---------|--------------------|--------|---------|-----|-------|
| [streams/core](/docs/core/introduction) | `2.0.x-dev` | `2.0` | `^10\|^11\|^12` | `^8.2` | Stable tags exist only for the old 1.x line (latest `v1.10.4`). |
| [streams/ui](/docs/ui/introduction) | `1.0.x-dev` | `1.0` | via Core | via Core | Requires Livewire `^3.0`. |
| [streams/api](/docs/api/introduction) | `1.0.x-dev` | `1.0` | via Core | `^8.2` | |
| [streams/sdk](/docs/sdk/introduction) | `1.0.x-dev` | `1.0` | via Core | `^8.2` | Install as a dev dependency. The optional [MCP server](/docs/mcp) needs `laravel/mcp`, so Laravel 11.45+ or 12.41+. |
| [streams/testing](/docs/testing/introduction) | `1.0.x-dev` | `1.0` | **10 only, for now** | via Core (8.2+) | Requires `orchestra/testbench ^8.36`, which targets Laravel 10. Being widened to `^8.36\|^9.15\|^10.8` (Laravel 10 to 12). |
| [streams/mongodb](/docs/core/sources-and-adapters) | `1.0.x-dev` | `1.0` | via Core | via Core | Experimental. Requires `mongodb/mongodb ^1.10`. |
| [@laravel-streams/api-client](/docs/client/introduction) | `3.0.0` | `master` | n/a | n/a | npm package, zero runtime dependencies. |
| `streams/streams` (starter) | `1.0.x-dev` | `1.0` | `^10.0` | `^8.0.2` declared (Core needs 8.2+) | Pins Laravel 10; requires Core, UI, and API. |
"Via Core" means the package declares no framework or PHP constraint of its own and accepts whatever `streams/core ^2.0` accepts. Core, API, and SDK declare PHP `^8.2`, so every package needs PHP 8.2 or newer. The Laravel 10/11/12 [version matrix](/docs/installation#version-matrix) on the installation page shows the same support per Laravel version.
### What is actually tested
- Core, UI, and API run their CI on **Laravel 10** (10.49). The shared harness, `streams/testing`, is built on testbench 8, which is Laravel 10 only. Widening it to testbench 9 and 10 is in progress.
- Core **declares** Laravel 11 and 12 support, but the Core, UI, and API suites don't run on them until `streams/testing` is widened. Treat 11 and 12 as expected to work, not verified.
- `streams/sdk` has its own harness and runs on **Laravel 12.41** with `laravel/mcp`, so the SDK and its MCP server are tested on Laravel 12.
- This site (streams.dev) runs **Laravel 12** on PHP `^8.2`, with `streams/core` from `rc/prep` (aliased as `2.0.x-dev`), `streams/ui 1.0.x-dev`, and `streams/sdk` from `sdk/rc` as a dev dependency. It doesn't use `streams/testing`, so its own test suite runs on Laravel 12 and PHPUnit 11.
## Requiring the packages
Because there are no stable 2.x (Core) or 1.x (everything else) tags, Composer's default `minimum-stability: stable` won't find them. Worse, a bare `composer require streams/core` silently installs **Core 1.10.4**, the previous major version.
Require the development branches explicitly, and require Core alongside anything that depends on it:
```bash
composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev
composer require streams/core:2.0.x-dev streams/api:1.0.x-dev
composer require --dev streams/sdk:1.0.x-dev
```
Or allow dev packages project-wide while still preferring stable releases of everything else:
```json
{
"minimum-stability": "dev",
"prefer-stable": true
}
```
After that, `^2.0` for Core and `^1.0` for the others resolve to the development branches.
## Versioning policy
The plan is to adopt [semantic versioning](https://semver.org) and tag stable releases (Core 2.0, and 1.0 for UI, API, SDK, and Testing) as part of the release-candidate work. Until tags exist:
- Development branches can change without notice. Pin a commit (`2.0.x-dev#abc1234`) if you need a fixed point.
- Breaking changes are listed in the [Changelog](/docs/changelog) and explained in the [Upgrade guide](/docs/upgrading).
- The JavaScript client is already semver-tagged on npm (`3.0.0`).
## Related
- [Installation](/docs/installation)
- [Upgrade guide](/docs/upgrading)
- [Changelog](/docs/changelog)
---
# Get started: Changelog
> Notable changes to the Streams packages, newest first, grouped by month.
Source: https://streams.dev/docs/changelog
Streams packages are pre-release and installed from development branches, so this log is organized by month and branch rather than by version. Entries come from each repository's commit history. Changes that need action on your side are marked **Breaking** and explained in the [Upgrade guide](/docs/upgrading).
## September 2026
- **SDK (`1.0` release candidate):** Added a local-development [MCP server](/docs/mcp) (`php artisan mcp:start streams`, needs `laravel/mcp` on Laravel 11.45+ or 12.41+) with 14 tools, 2 resources, and the `design-stream` prompt. Added [`streams:validate`](/docs/sdk/commands#streamsvalidate) and [`streams:list --json`](/docs/sdk/commands#streamslist), and the [stream definition schema](/docs/sdk/stream-schema), served at `/schema/streams.schema.json`. `streams:livewire` now generates Livewire 3 components in `App\Livewire`. **Breaking:** `streams:admin` was removed, and `make:stream` and `streams:livewire` refuse to overwrite files without `--force`. The SDK now requires PHP 8.2.
- **Core, API (release candidates):** Require PHP 8.2. Core supports the Laravel 11 and 12 filesystem contract and Carbon 3.
- **Docs:** streams.dev is now the single source of truth for package documentation. Added [Versions and support](/docs/versions), this changelog, the [Upgrade guide](/docs/upgrading), `/llms.txt`, `/llms-full.txt`, and raw markdown for every page (append `.md` to any docs URL). Also added [Agents](/docs/agents), [MCP](/docs/mcp) (then planned; it has since shipped in `streams/sdk`), [Workflows](/docs/workflows), [Tenancy](/docs/tenancy), [Theming](/docs/ui/theming), and the [SDK command reference](/docs/sdk/commands).
- **UI (`1.0`):** HTML attribute support on more components.
## August 2026
- **Core (`2.0`):** Added `elasticsearch` and `opensearch` source adapters built on a shared `AbstractSearchIndexAdapter`. Configure OpenSearch connections under `streams.core.opensearch`. The client libraries are suggested dependencies, not required. See [Sources and adapters](/docs/core/sources-and-adapters).
- **UI (`1.0`):** **Breaking.** The OpenSearch adapter moved from UI to Core.
- **UI (`1.0`):** Accessible `ChartWidget` with an empty state; table search is ANDed with active filters; modal and file-input close actions.
## July 2026
- **Core (`2.0`):** **Breaking.** `Criteria::with()` attaches eager-loaded relationships under the relation name (the field handle without a trailing `_id`, or the field's `relation` config) and leaves the foreign-key attribute as a scalar.
- **API (`1.0`):** `with` / `with[]` eager loading matches relation names, following the Core change.
- **UI (`1.0`):** Bulk actions, pages and navigation updates, vertical navigation style, `MenuItem` modal support, image builder, icon-only actions, and button size/radius options.
## June 2026
- **API (`1.0`):** **Breaking.** Endpoint builders with explicit route registration. Controllers under `Streams\Api\Http\Controller\…` became invokable endpoints under `Streams\Api\Endpoints\…`; routes are mounted per `ApiInterface`; access runs through the configurable `gate_middleware`. See [Custom interfaces](/docs/api/custom-interfaces).
- **API (`1.0`):** `API::tenant()` and `ApiInterface::tenant()` for per-request tenant resolution. See [Tenancy](/docs/api/tenancy).
- **Core (`2.0`):** Two-argument `where('field', $value)` handling fix.
- **UI (`1.0`):** Table row selection fixes, row attributes, form spacing, required flags, and unified table filter state.
## May 2026
- **Core (`2.0`):** `Criteria::whereIn()`; `getIdAttribute()` on the Streams trait.
- **UI (`1.0`):** FullCalendar builder, modal header and footer components, grid IDs, bulk action improvements, and form state path prefixes.
## January to April 2026
- **UI (`1.0`):** Timeline component, panel wizard, breadcrumbs, `Action` extends `MountableAction`, file uploads on pages, and table views.
- **API (`1.0`):** Minor fixes to the entry show endpoint.
- **Client (`3.0.0`):** Tagged January 7 and published to npm from GitHub Actions. Version 3 is a zero-dependency JavaScript rewrite of the TypeScript client.
## Related
- [Upgrade guide](/docs/upgrading)
- [Versions and support](/docs/versions)
---
# Get started: Upgrade guide
> How to stay current on the development branches, and what to change for each breaking change.
Source: https://streams.dev/docs/upgrading
Streams packages are installed from development branches until stable tags exist (see [Versions and support](/docs/versions)). Upgrading means pulling the latest commit on your branch and handling any breaking changes listed below. The [Changelog](/docs/changelog) has the full list.
## Updating
```bash
composer update streams/core streams/ui streams/api --with-dependencies
composer update streams/sdk streams/testing
```
Then clear cached config and views, and run your tests:
```bash
php artisan optimize:clear
php artisan test
```
Commit `composer.lock` so your team and CI run the same commits.
## Breaking changes
### SDK: `streams:admin` removed, `--force` required (September 2026)
The `streams/sdk` release candidate changes its generators:
- `streams:admin` is gone. Generate the index, form, and show components with `php artisan streams:livewire {stream}` and put them behind your own layout and routes. Files it generated before are yours and keep working.
- `streams:livewire` writes Livewire 3 classes to `config('livewire.class_namespace')` (default `App\Livewire`, not `App\Http\Livewire`) and views to `resources/views/livewire/{stream}-{type}.blade.php`. It generates all three components unless you pass `--type`.
- `make:stream` and `streams:livewire` stop instead of overwriting existing files. Pass `--force` to overwrite.
- The SDK requires PHP 8.2. The optional MCP server needs `laravel/mcp`, which needs Laravel 11.45+ or 12.41+.
See the [command reference](/docs/sdk/commands).
### OpenSearch adapter moved to Core (August 2026)
The OpenSearch adapter was removed from `streams/ui` and added to `streams/core`, next to a new Elasticsearch adapter.
- Streams using `"source": {"type": "opensearch"}` keep working once Core is updated. Core resolves `opensearch` and `elasticsearch` on its own.
- If you referenced the class directly, change `Streams\Ui\Criteria\Adapter\OpenSearchAdapter` to `Streams\Core\Criteria\Adapter\OpenSearchAdapter`.
- Move connection config from `streams.opensearch.*` to `streams.core.opensearch.*` (publish `config/streams/core.php` or set `OPENSEARCH_HOST`, `OPENSEARCH_USERNAME`, `OPENSEARCH_PASSWORD`, and `OPENSEARCH_SSL_VERIFICATION`).
- Require `opensearch-project/opensearch-php` in your app. Core only suggests it.
### Eager loading uses relation names (July 2026)
`Criteria::with()` now takes the **relation name** and attaches the related entry under that name. The foreign-key attribute stays a scalar.
```php
// Before: the related entry replaced the foreign key
$post = Streams::entries('posts')->with(['author_id'])->first();
$post->author_id; // Entry
// After: ask for the relation name
$post = Streams::entries('posts')->with(['author'])->first();
$post->author; // Entry
$post->author_id; // 42
```
The relation name is the field handle with a trailing `_id` removed, or the relationship field's `relation` config if set. API clients change `with=author_id` to `with=author` the same way.
### API endpoint builders and explicit interfaces (June 2026)
`streams/api` was rebuilt around interfaces, resources, and invokable endpoints.
| Before | After |
|--------|-------|
| `Streams\Api\Http\Controller\Entries\GetEntries` | `Streams\Api\Endpoints\Entries\ListEntries` |
| `Streams\Api\Http\Controller\Streams\GetStreams` | `Streams\Api\Endpoints\Streams\ListStreams` |
| Other `Streams\Api\Http\Controller\{Entries,Streams}\*` | Same class name under `Streams\Api\Endpoints\{Entries,Streams}\*` |
| `SetUpInterface` middleware, alias `interface`, pushed into the `api` group | `SetUpApiInterface`, alias `api.interface`, applied to API routes only |
| Route names always included the interface ID (`streams.api.api.entries.list`) | The default interface omits it (`streams.api.entries.list`); other interfaces keep it (`streams.api.v1.entries.list`) |
| `enabled` config was informational | `enabled` is enforced by `gate_middleware`; API routes return 404 until `STREAMS_API_ENABLED=true` |
To upgrade:
1. Set `STREAMS_API_ENABLED=true` wherever the API should respond.
2. Register routes with `API::routeCrud()` (or `routeEntries()` / `routeStreams()`) or `API::interface(...)` from a service provider's `boot()` method, not inside a prefixed route group. See [API installation](/docs/api/installation).
3. Move custom controllers that extended the old controllers onto the new endpoint classes. See [Custom endpoints](/docs/api/custom-endpoints).
4. Move authentication into interface middleware or your own gate. See [API authentication](/docs/api/authentication).
## Core 1.x to 2.0
Core 2.0 is a separate branch from the 1.x line (last tag `v1.10.4`) and is what every current Streams package requires. There is no written migration guide from 1.x yet. If you are on 1.x, start from the [Core introduction](/docs/core/introduction) and treat 2.0 as a new install.
## Related
- [Changelog](/docs/changelog)
- [Versions and support](/docs/versions)
---
# Guides: Core Concepts: Architecture
> How Streams packages fit together in a Laravel application.
Source: https://streams.dev/docs/architecture
## Overview
A Streams application is a normal Laravel project with additional conventions. Streams does not replace Laravel routing, config, or deployment—it extends them.
```mermaid
flowchart TB
subgraph git [In version control]
StreamsJSON["streams/*.json"]
StreamData["streams/data/"]
LaravelConfig["config/ + .env"]
end
subgraph packages [Composer packages]
Core[streams/core]
UI[streams/ui]
API[streams/api]
end
subgraph runtime [Runtime]
Entries[Entries and repositories]
CP[Control panel]
REST[REST API]
end
StreamsJSON --> Core
StreamData --> Core
Core --> Entries
Core --> UI
Core --> API
UI --> CP
API --> REST
```
## What your team commits
| Path | Purpose |
|------|---------|
| `streams/*.json` | Stream definitions—fields, sources, routes, UI config |
| `streams/data/` | Entry data when using filebase storage |
| `config/streams/` | Published package configuration |
| `app/` | Custom PHP when JSON is not enough |
Environment-specific values stay in `.env`, not in stream JSON.
## Streams Core
Core loads stream definitions, resolves data sources (filebase, Eloquent, collections, remote), and exposes repositories and criteria for querying entries.
Hub guide: [Streams](/docs/streams)
Reference: [Core documentation](/docs/core/introduction)
## Streams UI
UI builds control panels, forms, tables, and pages from stream and panel configuration. It runs on Livewire and Tailwind—familiar Laravel frontend stack.
Hub guide: [UI](/docs/ui)
Reference: [UI documentation](/docs/ui/introduction)
## Streams API
API registers REST routes for streams you expose. Response format follows the existing Streams API envelope (see [Responses](/docs/api/responses)).
Hub guide: [API](/docs/api)
Reference: [API documentation](/docs/api/introduction)
## Composing packages
| Goal | Typical packages |
|------|------------------|
| Data layer only | `streams/core` |
| Admin for your team | `streams/core`, `streams/ui` |
| Headless + admin | `streams/core`, `streams/ui`, `streams/api` |
| Faster scaffolding | Add `streams/sdk` (dev) |
| CI and package tests | Add `streams/testing` (dev) |
See [Use cases](/docs/use-cases) for concrete starting points.
---
# Guides: Core Concepts: Streams
> Get started with the stream modeling engine.
Source: https://streams.dev/docs/streams
## Introduction
The Streams platform leans heavily on domain-driven design (DDD). We call these domain abstractions `streams`, hence our namesake.
An example could be configuring a domain model (a stream) for a website's pages, users of an application, or feedback submissions from a form. Streams describe your data structures.
## Defining Streams
Using JSON files, you can define stream configurations in the `streams/` directory. The filenames serve as the stream's `id`.
It is highly encouraged to use the plural form of a noun when naming Streams—for example, contacts and people. Also, naming conventions like `business_contacts` or `neat-people` work well.
```files
├── streams/
│ ├── users.json
│ ├── pages.json
│ └── contacts.json
```
### The Basics
To get started, you need only specify the `id`, which is the filename itself, and some `fields` to describe the domain object's structure.
Let's create a little stream to hold information for a simple CRM.
In `streams/contacts.json`:
```json
{
"name": "Contacts",
"description": "A simple address book.",
"config": {
"source": {
"type": "filebase",
"path": "streams/data/contacts",
"format": "json"
},
"abstract": "Streams\\Core\\Entry\\Entry",
"criteria": "Streams\\Core\\Criteria\\Criteria",
"repository": "Streams\\Core\\Repository\\Repository",
"collection": "Illuminate\\Support\\Collection"
},
"fields": {
"name": "string",
"email": "email",
"company": {
"type": "relationship",
"config": {
"related": "companies"
}
}
}
}
```
### Fields
- [Fields](/docs/fields)
- [Field Types](/docs/fields#field-types)
**Fields** are an essential descriptor of the domain object. They describe what properties the domain object will have and how they work. Field **types** control things like accessors, data mutation, and casting.
The **field configuration keys** serve as a `handle`, which you can use to reference the field later. So, for example, you may access the above contact fields like this:
```php
$entry->email;
$entry->company->email;
```
### Stream Routes
- [Stream Routes](/docs/routing#stream-routes)
- [Route Options](/docs/core/routes)
Streams can simplify **routing** by defining associated routes in their definition.
In `streams/contacts.json`:
```json
{
"routes": {
"index": "contacts",
"view": "contacts/{id}"
}
}
```
You can also use an array to include other **route options**.
In `streams/contacts.json`:
```json
{
"routes": {
"contact": {
"csrf": false,
"uri": "form/{entry.email}"
}
}
}
```
### Stream Validation
Streams simplifies **validation** by defining validation in their definition.
- [Validation](/docs/core/validation)
In `streams/contacts.json`:
```json
{
"rules": {
"name": [
"required",
"max:100"
],
"email": [
"required",
"email:rfc,dns"
],
"company": "required|unique"
}
}
```
### Security
Specify the [Laravel policy](https://laravel.com/docs/authorization#creating-policies) class to use for the stream. There is no separate security guide.
In `streams/contacts.json`:
```json
{
"policy": "App\\Contacts\\ContactPolicy"
}
```
### Caching
Streams provides a touch-free caching system you can define in the configuration.
- [Caching](/docs/core/caching)
In `streams/contacts.json`:
```json
{
"config": {
"cache": {
"enabled": true,
"ttl": 1800,
"store": "file"
}
}
}
```
Caching is off unless `config.cache.enabled` is `true`. `ttl` is in seconds (default 3600), and `store` defaults to your default cache store.
### Sources
Sources define the source information for entry data which you can define in the configuration.
- [Sources and adapters](/docs/core/sources-and-adapters)
In `streams/contacts.json`:
```json
{
"source": {
"type": "filebase",
"format": "md"
}
}
```
## Stream Entries
Domain entities are called `entries` within the Streams platform. A stream defines entry attributes, or `fields`, that dictate the entry's properties, data-casting, and more.
- [Entries](/docs/core/entries)
### Abstracts
The **abstract** parameter defines the class to use when constructing entry instances.
- [Entries](/docs/core/entries)
In `streams/contacts.json`:
```json
{
"abstract": "App\\Contacts\\Contact"
}
```
> When defining Elqouent stream sources, the sources model will be used as the abstract.
### Criteria
The **criteria** parameter defines the class to use when building entry queries.
- [Criteria](/docs/core/criteria)
In `streams/contacts.json`:
```json
{
"criteria": "App\\Contacts\\ContactCriteria"
}
```
### Repositories
The **repository** parameter defines the repository class to use for the stream entries.
- [Repositories](/docs/core/repositories)
In `streams/contacts.json`:
```json
{
"repository": "App\\Contacts\\ContactRepository"
}
```
## Advanced Streams
### JSON References
You can use JSON file references within stream configurations to point to other JSON files using the `@` symbol followed by a relative path to the file. In this way, you can reuse various configuration information or tidy up larger files. **The referenced file's JSON data directly replaces the reference.**
In `streams/contacts.json`:
```json
{
"name": "Contacts",
"fields": "@streams/fields/contacts.json"
}
```
In `streams/fields/contacts.json`:
```json
{
"name": "string",
"email": "email",
"company": {
"type": "relationship",
"stream": "company"
}
}
```
### Extend a Stream
A stream can `extend` another stream, which works like a recursive **merge**.
In `streams/family.json`:
```json
{
"name": "Family Members",
"extend": "contacts",
"fields": {
"relation": {
"type": "select",
"config": {
"options": {
"mother": "Mother",
"father": "Father",
"brother": "Brother",
"sister": "Sister"
}
}
}
}
}
```
In the above example, all `contacts` fields are available to you, as well as the new `relation` field.
```php
$entry->email; // The email value.
$entry->relation; // The relation value.
```
### Stream Sources
You can configure the flat-file database as well as other sources for storing data including any Laravel database. No code changes required.
For full reference, see [Core — Streams](/docs/core/streams), [Core — Caching](/docs/core/caching), and [Core — Validation](/docs/core/validation).
---
# Guides: Core Concepts: Fields
> Fields, types, and inputs are documented here.
Source: https://streams.dev/docs/fields
## Introduction
Fields represent the type and characteristics of your stream data. For example a "name" field would likely be a **string** field type.
Fields are strictly concerned with data. Please see the [UI package](/docs/ui/introduction) for configuring field [inputs](/docs/ui/inputs).
## Defining Fields
Fields can be defined within the JSON [configuration for your streams](/docs/streams#defining-streams). You can get started by simply defining fields by `handle` and their `type` respectively.
#### Basic Example
In `streams/contacts.json`:
```json
{
"fields": [
{
"handle": "title",
"type": "string"
}
]
}
```
#### Full Example
To define more information about the field use an array:
In `streams/contacts.json`:
```json
{
"fields": [
{
"handle": "title",
"name": "Title",
"description": "The title of the film.",
"type": "string",
"rules": ["min:4"],
"config": {
"default": "Untitled"
},
"example": "Star Wars: The Force Awakens",
"protected": false
}
]
}
```
### Field Validation
Define [Laravel validation rules](https://laravel.com/docs/validation#available-validation-rules) for fields and they will be merged the [stream validation rules](/docs/streams#stream-validation).
In `streams/contacts.json`:
```json
{
"fields": [
{
"handle": "name",
"type": "string",
"rules": ["required", "max:100"]
},
{
"handle": "email",
"type": "email",
"rules": ["required", "email:rfc,dns"]
},
{
"handle": "company",
"type": "string",
"rules": ["required", "unique"]
}
]
}
```
## Basic Usage
Values are stored as an [image source](/docs/images#image-sources)
```php
Image::make($entry->profile_image)->url();
```
### Field Decorators
Field decorators provide expanded function to entry attributes like a universal presenter.
The below example demonstrates the `image` field decorator:
```php
$entry->decorate('profile_image')->url();
```
You may also use magic methods derived from "camel casing" the field's handle to invoke decoration.
```php
$entry->profileImage()->url();
```
## Field Types
The field type is responsible for validating, casting, and more for its specific data type. These are the 24 types registered by Core (`streams.core.field_types`); apps and addons can register more. Each example is a complete field in list form, so it validates against the [stream definition schema](/docs/sdk/stream-schema).
### String
```json
{
"handle": "title",
"type": "string"
}
```
### URL
```json
{
"handle": "website",
"type": "url"
}
```
### UUID
`"default": true` in `config` generates a UUID when the attribute is missing.
```json
{
"handle": "id",
"type": "uuid",
"config": {
"default": true
}
}
```
### Hash
```json
{
"handle": "password",
"type": "hash"
}
```
### Slug
`config.separator` sets the word separator (default `-`).
```json
{
"handle": "slug",
"type": "slug",
"config": {
"separator": "-"
}
}
```
### Email
```json
{
"handle": "email",
"type": "email"
}
```
### Encrypted
```json
{
"handle": "api_token",
"type": "encrypted"
}
```
### Color
Values are stored lowercase and must parse as a color. The decorator adds `hex()`, `rgb()`, `rgba()`, and the individual channels; `config.format` picks the default output (`hex`).
```json
{
"handle": "brand_color",
"type": "color"
}
```
### Number
```json
{
"handle": "rating",
"type": "number"
}
```
### Integer
```json
{
"handle": "age",
"type": "integer"
}
```
### Decimal
`config.precision` rounds to that many decimal places.
```json
{
"handle": "price",
"type": "decimal",
"config": {
"precision": 2
}
}
```
### Boolean
```json
{
"handle": "published",
"type": "boolean"
}
```
### Date
`config.format` is the storage format (default `Y-m-d`).
```json
{
"handle": "birthday",
"type": "date"
}
```
### Time
`config.format` defaults to `H:i:s`; `config.timezone` defaults to `app.timezone`.
```json
{
"handle": "opens_at",
"type": "time"
}
```
### Datetime
`config.format` defaults to `Y-m-d H:i:s`; `config.timezone` defaults to `app.timezone`.
```json
{
"handle": "published_at",
"type": "datetime",
"config": {
"timezone": "UTC"
}
}
```
### Enum
`enum` is an alias of `select`. Both need `config.options`.
```json
{
"handle": "size",
"type": "enum",
"config": {
"options": {
"sm": "Small",
"lg": "Large"
}
}
}
```
### Select
```json
{
"handle": "status",
"type": "select",
"config": {
"options": {
"draft": "Draft",
"live": "Live"
}
}
}
```
### Multiselect
```json
{
"handle": "tags",
"type": "multiselect",
"config": {
"options": {
"news": "News",
"guides": "Guides"
}
}
}
```
### Array
`config.items` lists the allowed item types. Each item must pass at least one of them. Set `config.enforce_items` to `false` to skip the check, or use `config.related` (or `config.stream`) to cast items to entries of a stream.
```json
{
"handle": "scores",
"type": "array",
"config": {
"items": [
{"type": "string"},
{"type": "number"}
]
}
}
```
### Object
`config.allowed` lists the value types an object may be. Each item names a `stream` (an entry of that stream), a `generic` class, or a `prototype` class. Without `allowed`, any object is accepted.
```json
{
"handle": "address",
"type": "object",
"config": {
"allowed": [
{"stream": "addresses"},
{"generic": "Illuminate\\Support\\Collection"}
]
}
}
```
### File
The value is a path string. The decorator adds file helpers. Core does not restrict file types; add Laravel `rules` for that.
```json
{
"handle": "attachment",
"type": "file"
}
```
### Image
A `file` whose decorator works with the [image manager](/docs/images).
```json
{
"handle": "profile_image",
"type": "image"
}
```
### Relationship
`config.related` names the related stream. `related` next to `type` is ignored. Add `"multiple": true` to store a list of keys, and `key_name` if the related stream is not keyed by `id`.
```json
{
"handle": "author_id",
"type": "relationship",
"config": {
"related": "authors"
}
}
```
### Polymorphic
Stores a reference to an entry of any stream as `{"@stream": "posts", "id": "..."}` and restores the entry when read. `config.related` optionally documents the streams you expect; Core does not enforce it.
```json
{
"handle": "commentable",
"type": "polymorphic",
"config": {
"related": ["posts", "pages"]
}
}
```
---
# Guides: The Basics: Files
> Files and image handling.
Source: https://streams.dev/docs/files
## Introduction
Streams comes with a simple wrapper for the Laravel filesystem.
### Files Stream
First, define a stream for the filesystem data:
```json
{
"name": "Files",
"handle": "files",
"description": "Basic filesystem cache.",
"config": {
"source": {
"format": "json"
}
},
"fields": [
{
"handle": "id",
"type": "uuid",
"required": true,
"unique": true,
"config": {
"default": true
}
},
{
"handle": "path",
"type": "string",
"required": true
},
{
"handle": "is_dir",
"type": "boolean",
"required": true
},
{
"handle": "disk",
"type": "string",
"required": true
},
{
"handle": "name",
"type": "string",
"required": true
},
{
"handle": "size",
"type": "integer"
},
{
"handle": "mime_type",
"type": "string"
},
{
"handle": "visibility",
"type": "string"
},
{
"handle": "last_modified",
"type": "datetime"
},
{
"handle": "extension",
"type": "string"
}
]
}
```
### Configuration
To get started, define the data stream for the filesystem disk you wish to use.
```php
// config/filesystems.php
// ...
'disks' => [
'local' => [
'stream' => 'files',
],
],
```
## Filesystem Wrapper
The `StreamFilesystem` decorates Laravel's configured filesystem. In order to leverage streams fully you will need to use this wrapper to interact with the filesystem. The wrapper exists only to sync filesystem data to the configured stream.
```php
$filesystem = Streams::filesystem(string $disk);
// If you happen to have the stream.
$filesystem = $stream->filesystem(string $disk);
// Use as normal.
$filesystem->put($path, $content);
```
### Indexing Files
Use the `index` method to index the filesystem to the configured stream.
```php
$filesystem->index(string $path = '/');
```
### Filesystem Data
After indexing, the filesystem data can be used normally.
```php
$images = Streams::entries('files')
->where('extension', 'jpg')
->orderBy('size', 'asc')
->get();
```
---
# Guides: The Basics: Content
> Filebase pages and content fragments in Streams.
Source: https://streams.dev/docs/content
## Introduction
Streams stores content as **entries** in configured sources. This hub page covers common content patterns; field types and adapters are documented in [Core](/docs/core/introduction).
## Pages stream
A typical pages stream uses the **filebase** source (default) with HTML or Markdown files:
```json
{
"id": "pages",
"config": {
"source": {
"type": "filebase",
"format": "html"
}
},
"routes": [
{
"handle": "view",
"uri": "{uri}",
"parse": true,
"view": "{layout}"
}
],
"fields": [
{ "handle": "id", "type": "slug", "required": true, "unique": true },
{ "handle": "title", "type": "string", "required": true },
{ "handle": "uri", "type": "string", "required": true, "unique": true },
{ "handle": "body", "type": "string" }
]
}
```
### Entry frontmatter
Each file in `streams/data/pages/` carries YAML frontmatter plus a Blade/HTML body:
```html
---
title: Welcome
uri: /
layout: blank
---
@include('partials.topbar')
{{ $entry->title }}
```
| Key | Role |
|-----|------|
| `uri` | URL path for `parse: true` routes |
| `layout` | Blade layout passed to `{layout}` in the route definition |
| `title` | Stored field; available on `$entry` |
See [Site pages](/docs/site-pages) for how this repo wires `/`, `/docs`, and `/addons`.
## Markdown documentation
Doc streams set `format: md` and route to a shared view:
```json
{
"routes": [
{
"uri": "docs/{id}",
"view": "docs"
}
]
}
```
Files live in `streams/data/docs/` (or `{package}_docs/`). The entry `id` matches the filename without extension.
## Posts and structured content
Blog or article streams follow the same pattern with different fields — for example `slug`, `published_at`, and a `relationship` to authors. Model fields in stream JSON; store entries in filebase or [database sources](/docs/core/sources-and-adapters).
## Blocks
Block content is an array field. `config.stream` turns each item into an entry of a stream, either a stream ID or an inline definition, so every block gets fields, casting, and decorators:
```json
{
"handle": "content",
"type": "array",
"config": {
"stream": {
"id": "content_blocks",
"fields": [
{ "handle": "type", "type": "select", "config": { "options": ["text", "gallery"] } },
{ "handle": "title", "type": "string" },
{ "handle": "body", "type": "string" }
]
}
}
}
```
An item saved from an existing entry keeps an `@stream` key (for example `{"@stream": "gallery_blocks", "id": "summer"}`) and is restored as that entry when read. To limit what items may be, list types in `config.items` (for example `[{"type": "string"}]`).
Each block type can reference another stream or an inline field structure.
## Partials via Includes
For reusable view fragments, use Core's [Includes](/docs/core/views-and-includes) API rather than duplicating Blade `@include` paths in JSON:
```php
Includes::include('sidebar', 'partials.sidebar');
```
## Related
- [Site pages](/docs/site-pages)
- [Routing](/docs/routing)
- [Core streams](/docs/core/streams)
- [Core entries](/docs/core/entries)
---
# Guides: The Basics: Routing
> Routing your application.
Source: https://streams.dev/docs/routing
## Introduction
All requests to your application are handled by **Laravel** unless you create the routes using one of the specific methods described below.
## Defining Routes
The Streams platform has a couple of ways it routes requests, which are listed below. Otherwise, [standard Laravel routing applies](https://laravel.com/docs/routing).
### Route Files
You can configure routes just as you would in a regular Laravel application using the `routes/web.php` file.
### Streams Router
The Streams platform provides a `Route::streams()` method for defining routes. *All streams-specific routing approaches pass through this method.*
```php
// Options
Route::streams('uri', [
'foo' => 'bar',
]);
// View
Route::streams('uri', 'view');
// Controller
Route::streams('uri', 'App\Http\Controller\Example@show');
// Controller and more
Route::streams('uri', [
'uses' => 'App\Http\Controller\Example@show'
]);
```
The first argument is the URI and the second is either:
- The name of the [view](/docs/core/views-and-includes) to render.
- A callable string.
- An array of [route options](#route-options).
### Stream Routes
Defining routes in your [stream configuration](/docs/streams#routing) makes it easy to automate naming and URL generation around your domain information and entities.
Define stream routes using a `action => options` format, where `options` is again either the URI, controller and method string, or an array of [route options](#route-options).
In `streams/contacts.json`:
```json
{
"routes": {
"index": {
"uri": "contacts",
"view": "contacts"
},
"view": {
"uri": "contacts/{id}",
"view": "contact"
},
"profile": {
"uri": "contacts/{id}",
"view": "profile"
}
}
}
```
#### Automatic Naming
Unless a [route name](#named-routes) is specified, stream configured routes automatically name themselves like `streams::{stream}.{action}`.
```php
$url = route('streams::contacts.index');
```
#### Automatically Resolved Views
Unless a view is specified, the associated requests will attempt to resolve a view automatically.
In `streams/contacts.json`:
```json
{
"routes": {
"index": {
"uri": "contacts"
},
"view": {
"uri": "contacts/{id}"
}
}
}
```
`EntryController` picks the first view that exists, in this order:
1. The route's `view` option.
2. A view with the same name as the route (`as`).
3. For a route that resolves an entry (`contacts/{id}`), the singular stream ID (`resources/views/contact.blade.php`). Otherwise, the plural (`resources/views/contacts.blade.php`).
If none exists, no view is set.
## Route Parameters
The Streams platform adds support for deep parameter variables using a dot notation when using the `URL::streams()` method to [generate URLs](#generating-urls).
```php
URL::streams('uri/{foo.bar}', 'view');
```
### Stream Parameter
You can specify the stream associated with the route using the [route option](#streams) or by using the `{stream}` URI segment variable in your URI pattern to resolve the stream by its handle.
```php
Route::streams('address-book/{stream}', 'contacts');
```
Consider locking down this routing pattern using a [parameter constraint](#parameter-constraints).
```php
Route::streams('address-book/{stream}', [
'view' => 'contacts.list',
'constraints' => [
'stream' => '(businesses|family)'
],
]);
```
The resolved stream will be available within the view:
```blade
@verbatim
{{ $stream->name }}
@foreach ($stream->entries()->get() as $entry)
{{ $entry->name }}
@endforeach
@endverbatim
```
### Entry Parameters
You can specify a stream entry associated with the route using the [route option](#entries) or by using the `{id}` URI segment variable in your URI pattern to resolve the entry by its ID or handle.
```php
Route::streams('address-book/{stream}/{id}', 'contacts');
```
You can also use `{entry.*}` parameters to query the entry by its field values.
```php
// address-book/contacts/ryan@example.com
Route::streams('address-book/{stream}/{entry.email}', 'contacts');
```
The first matching entry will be available within the view:
```php
@verbatim
{{ $entry->name }}
@endverbatim
```
A `404` error page will be displayed entry resolution is attempted, but no entry is found.
## Route Options
All Streams platform-specific methods of registering routes support the following route options.
All route options are parsed with [controller data](/docs/core/routes):
```php
Route::streams('address-book/{stream}/{id}', [
'view' => '{streams.handle}',
]);
```
### View
Use the `view` option to specify a [view](/docs/core/views-and-includes) to render:
```php
Route::streams('uri', [
'foo' => 'bar',
'view' => 'example',
]);
```
### Stream
Use the `stream` option to specify the stream associated with the request. [Stream configured routes](#stream-routes) will do this automatically.
```php
Route::streams('uri', [
'stream' => 'contacts',
]);
```
The stream is automatically injected into the view:
```blade
@verbatim
{{ $stream->name }}
@foreach ($stream->entries()->get() as $entry)
{{ $entry->name }}
@endforeach
@endverbatim
```
### Entry
You can also specify a specific entry identifier:
```php
Route::streams('uri', [
'stream' => 'contacts',
'entry' => 'john_smith',
]);
```
The stream entry is automatically injected into the view:
```blade
// uri/ryan_thompson
@verbatim
{{ $entry->name }}
@endverbatim
```
You can use entry fields to query entries for the view.
```php
// uri/ryan@example.com
Route::streams('uri/{entry.email}', [
'stream' => 'contacts',
]);
```
You can also hard code the entry ID or handle:
```php
Route::streams('uri', [
'stream' => 'contacts',
'entry' => 'ryan_thompson',
]);
```
The first result is automatically injected into the view:
```blade
// uri/ryan_thompson
@verbatim
{{ $entry->name }}
@endverbatim
```
### Redirect
Use the `redirect` and optional `status_code` option to specify a redirect:
```php
Route::streams('uri/{entry.name}', [
'redirect' => '/new/uri',
'status_code' => 301, // Default
]);
```
Redirects highlight a good use case to leverage the fact that route options are parsed with controller data:
```php
Route::streams('uri/{entry.name}', [
'redirect' => '/new/uri/{stream.id}/{entry.name}',
'status_code' => 301, // Default
]);
```
#### Native Redirects
You can create [Laravel redirects](https://laravel.com/docs/routing#redirect-routes) in your `routes/web.php` using the `Route` facade as well:
``` php
Route::redirect('/from', '/to');
Route::redirect('/from', '/to', 301);
Route::permanentRedirect('/from', '/to');
```
### Named Routes
Use the `as` option to specify the name of the route:
```php
Route::streams('uri', [
'view' => 'example',
'as' => 'login',
]);
```
You can refer to the route by name using the typical Laravel methods:
```php
$url = route('login');
```
### HTTP Verbs
Use the `verb` option to specify the HTTP verb the route should respond to:
```php
Route::streams('uri', ['verb' => 'any']);
Route::streams('uri', ['verb' => 'get']); // Default
Route::streams('uri', ['verb' => 'put']);
Route::streams('uri', ['verb' => 'post']);
Route::streams('uri', ['verb' => 'patch']);
Route::streams('uri', ['verb' => 'delete']);
Route::streams('uri', ['verb' => 'options']);
```
### Route Middleware
Use the `middleware` option to assign additional middleware to the route:
```php
Route::streams('uri', [
'middleware' => ['first', 'second']
]);
```
### Parameter Constraints
Use the `constraints` option to specify allowed parameter formatting for the route using regular expression:
```php
Route::streams('uri/{name}', [
'constraints' => ['name' => '[A-Za-z]+']
]);
```
Laravel does not support dots in parameter names at this time. For this reason, `{entry.name}` type parameters transform into `{entry__name}`.
```php
Route::streams('uri/{entry.name}', [
'constraints' => ['entry__name' => '[A-Za-z]+']
]);
```
### Disabling CSRF
You can disable CSRF protection using the **csrf** option.
```php
Route::streams('uri', [
'csrf' => false
]);
```
### Deferring Routes
Use the `defer` option to defer registering a route.
```php
Route::streams('/{id}', [
// ...
'defer' => true,
]);
```
## Generating URLs
You may use the `URL::streams()` method to generate URLs for named routes, including those with dotted parameter variables. This method also supports parsing URL strings with parameter data. The `extra` data argument is appending as a query string. Use the `absolute` argument to control whether the resulting URL is absolute or not.
```php
URL::streams($target, $parameters = [], $extra = [], $absolute = true);
$entry = Streams::entries('contacts')->first();
// contacts/{entry.email}/{entry.id}
$url = URL::streams('streams::contacts.view', ['entry' => $entry]);
// contacts/{email}/{id}
$url = URL::streams('streams::contacts.view', $entry);
```
You can also use [Laravel URL generation](https://laravel.com/docs/routing#named-routes) for named routes, though dotted parameters are not supported using Laravel methods:
```php
// Generating URLs.
$url = route('streams::contacts.index');
// Generating Redirects.
return redirect()->route('streams::contacts.index');
```
## Error Pages
Errors render views based on the status code of the error. For example, a `404` error will look a view in `resources/views/errors/{status_code}.blade.php`.
Laravel will automatically render a `404` page for any unhandled routes.
- [Laravel Custom Error Pages](https://laravel.com/docs/errors#custom-http-error-pages)
---
# Guides: Frontend: Images
> Read, resize, and render images with the Images facade, built on Intervention Image.
Source: https://streams.dev/docs/images
## Introduction
The Streams platform comes with a fluid and highly extensible image handling and manipulation tool that leans heavily on the fantastic [Intervention Image](https://github.com/Intervention/image).
## Reading Images
To get started, use the `Images` facade to create a new image for working with.
```php
use Streams\Core\Support\Facades\Images;
$image = Images::make('img/foo.jpg');
```
The facade is aliased for use in [views](/docs/core/views-and-includes) as well:
```blade
@verbatim{!! Images::make('resources/img/foo.jpg') !!}@endverbatim
```
### Image Sources
The first and only argument should be the source image to display. The following sources are supported out of the box:
#### Paths in the Filesystem
Any image path relative to the application root may be used.
```blade
@verbatim{!! Images::make('resources/img/foo.jpg') !!}@endverbatim
```
#### Configured Storage Disks
You may use any configured storage location as an image source.
```blade
@verbatim{!! Images::make('public://img/foo.jpg') !!}@endverbatim
```
If the file is not found relative to the base path of your application, the default public disk will be attempted.
```blade
@verbatim{!! Images::make('img/foo.jpg') !!}@endverbatim
```
#### Remote URLs
The URL of a remote image may also be used. The `allow_url_fopen` PHP directive must be enabled to use remote image sources.
```blade
@verbatim{!! Images::make('https://example.com/img/foo.jpg') !!}@endverbatim
```
Remote images are cached locally. To use remote images without caching locally just use regular `` tags.
### Named Images
Use named images to register image variables:
#### Registering Images
You may regiter iamges by name using the **register** method:
```php
use Streams\Core\Support\Facades\Images;
Images::register('logo.jpg', 'images/logo.jpg');
```
```blade
@verbatim{!! Images::make('logo.jpg')->fit(300, 500)->quality(60) !!}@endverbatim
```
## Editing Images
After you initiat a new image instance with `Images::make()`, you can use the below manipulation methods.
Chain methods together for more comple manipulations.
```php
use Streams\Core\Support\Facades\Images;
$image = Images::make('img/foo.jpg')
->fit(300, 500)
->quality(60)
->orientate();
```
```blade
@verbatim{!! Images::make('resources/img/foo.jpg')
->fit(300, 500)
->quality(60)
->orientate() !!}@endverbatim
```
### Resizing Images
Use the following methods to resize images.
- [resize()](http://image.intervention.io/api/resize)
- [widen()](http://image.intervention.io/api/widen)
- [heighten()](http://image.intervention.io/api/heighten)
- [fit()](http://image.intervention.io/api/fit)
- [crop()](http://image.intervention.io/api/crop)
- [trim()](http://image.intervention.io/api/trim)
### Adjusting Images
Use the following methods to adjust various aspects of images.
- [encode()](http://image.intervention.io/api/encode)
- [gamma()](http://image.intervention.io/api/gamma)
- [brightness()](http://image.intervention.io/api/brightness)
- [contrast()](http://image.intervention.io/api/contrast)
- [colorize()](http://image.intervention.io/api/colorize)
- [greyscale()](http://image.intervention.io/api/greyscale)
- [invert()](http://image.intervention.io/api/invert)
- [mask()](http://image.intervention.io/api/mask)
- [flip()](http://image.intervention.io/api/flip)
#### quality()
Additionally, you may use the `quality` method to adjust the quality alone of JPG images.
```php
Images::make('img/foo.jpg')->quality(60);
```
### Applying Effects
Use the following methods to apply effects to images.
- [filter()](http://image.intervention.io/api/filter)
- [pixelate()](http://image.intervention.io/api/pixelate)
- [rotate()](http://image.intervention.io/api/rotate)
- [blur()](http://image.intervention.io/api/blur)
### Drawing
Use the following methods to draw on images.
- [text()](http://image.intervention.io/api/text)
- [pixel()](http://image.intervention.io/api/pixel)
- [line()](http://image.intervention.io/api/line)
- [rectangle()](http://image.intervention.io/api/rectangle)
- [circle()](http://image.intervention.io/api/circle)
- [ellipse()](http://image.intervention.io/api/ellipse)
### Macros
Macros are a basic method of [extending the Streams platform](/docs/core/extending-core).
#### Defining Macros
You can define macros in a service provider.
```php
use Streams\Core\Image\Image;
Image::macro('thumbnail', function () {
return $this->fit(148)->encode('jpg', 50);
});
```
#### Applying Macros
```php
$thumbnail = Images::make('img/foo.jpg')->thumbnail();
```
## Outputting Images
Use output methods to display image data from an image object. The `img` method is used by default.
#### img()
Use the `img` method to return an `` tag.
```blade
@verbatim{!! Images::make('img/foo.jpg') !!}@endverbatim
```
The first parameter can be an `alt` tag or array of attributes. If an alt tag is provided, the attributes can still be provided as a second parameter. Note this is the default output method when used in Blade.
```php
Images::make('img/foo.jpg')->img('Foo Bar Image', ['width' => '100'])
```
Note that unmatched methods will pass through to set attribute values.
```php
Images::make('img/foo.jpg')->width(100)->img('Foo Bar Image')
```
#### url()
Use the `url` method to output a URL to the image. The first argument may be an array of query string parameters to append. The second argument can be used to force secure URLs. If not specified, the URLs will use the protocol of the request. If
```php
Images::make('img/foo.jpg')->url()
// Append a manual version query parameter.
Images::make('img/foo.jpg')->url(['version' => 'v1'])
```
#### inline()
Use the `inline` method to return an `` tag with a **base64** encoded **src**.
```blade
@verbatim{!! Images::inline('img/foo.jpg') !!}@endverbatim
```
The first parameter can be an `alt` tag or array of attributes. If an alt tag is provided, the attributes can still be provided as a second parameter. Note this is the default output method when used in Blade.
```php
Images::make('img/foo.jpg')->inline('Foo Bar')
```
#### base64()
Use the `base64` method to return a base64 encoded string.
```blade
@verbatim@endverbatim
```
#### css()
Use the `css` method to return a `url()` string for use in CSS backgrounds.
```blade
@verbatim
@endverbatim
```
#### data()
The `data` method will return the contents of the image as a string.
```php
echo Images::make('img/foo.jpg')->data()
```
### Responsive Images
#### srcsets()
```php
Images::make('img/foo.jpg')
->srcsets([
'1x' => [
'resize' => 400,
'quality' => 60
],
'2x' => [
'resize' => 800,
'quality' => 90
],
'640w' => [
'resize' => 800,
'quality' => 90
]
]);
```
#### srcset()
```php
Images::make('img/foo.jpg')
->srcset([
'(min-width: 600px) 400px' => [
'intrinsic' => 400,
'resize' => 400,
'quality' => 60
],
'(min-width: 1600px) 800px' => [
'intrinsic' => 800,
'resize' => 800,
'quality' => 90
]
])->img();
```
#### picture()
```php
Images::make('img/foo.jpg')
->resize(1800) // Fallback
->picture([
'(min-width: 600px)' => [
'resize' => 400,
'quality' => 60
],
'(min-width: 1600px)' => [
'resize' => 800,
'quality' => 90
]
]);
```
---
# Guides: Frontend: Assets
> Asset and image management overview.
Source: https://streams.dev/docs/assets
## Overview
Streams Core manages assets and images through stream field types and the asset registry. Use image fields for uploads; use the asset system for compiled or public vendor assets.
## Images
See [Images](/docs/images) for manipulation, URLs, and storage disks.
## Reference
Implementation detail lives in Core:
- [Assets](/docs/core/assets)
- [Fields — image types](/docs/core/fields)
---
# Guides: Frontend: Components
> Blade and Livewire UI patterns with Streams.
Source: https://streams.dev/docs/components
## Overview
Streams UI provides Blade components and Livewire-driven builders for tables, forms, and layout. Use them inside panels or embed in your own views.
## Patterns
- **Panel pages** — full CP sections with navigation
- **Embedded tables** — list stream entries with filters and actions
- **Custom Livewire** — extend UI builders when JSON is not enough
Streams remains Laravel-native: standard views, middleware, and authorization apply.
## Learn more
- [UI introduction](/docs/ui/introduction)
- [TALL components (SDK)](/docs/sdk/tall-components)
- [Pages](/docs/ui/pages)
---
# Guides: Frontend: Forms
> When to use UI forms in Streams applications.
Source: https://streams.dev/docs/forms
## Overview
Streams UI generates forms from stream field definitions. Your team gets validation, field types, and layout without duplicating schema in Blade.
Use forms in control panels, settings pages, and modals.
## When to build a form
- Create or edit stream entries in the CP
- Multi-step product settings
- Nested or relationship fields
Define fields once in stream JSON; UI renders inputs from field types.
## Learn more
- [UI forms reference](/docs/ui/forms)
- [Field types](/docs/core/fields)
- [Control panel](/docs/control-panel)
---
# Guides: Advanced: API
> When and how to add a REST API to your Streams application.
Source: https://streams.dev/docs/api
## When to add Streams API
Add `streams/api` when your team needs to expose stream data to SPAs, mobile apps, partners, or webhooks—without writing CRUD controllers by hand.
Keep business logic in Laravel as usual. The API reads your stream definitions at request time, so new fields show up without new controllers.
## Installation
```bash
composer require streams/api:1.0.x-dev
```
Enable the gate and register routes from a service provider (not from `routes/api.php`):
```env
STREAMS_API_ENABLED=true
```
```php
// app/Providers/AppServiceProvider.php
use Streams\Api\Support\Facades\API;
public function boot(): void
{
API::routeCrud();
}
```
See [API installation](/docs/api/installation).
## What you get
Standard REST endpoints under `/api/streams/{stream}/entries`. Response shape follows the Streams envelope in [Responses](/docs/api/responses), not JSON:API.
## What you own
- **Authentication.** The API ships none. Add middleware to the interface or extend the gate. See [API authentication](/docs/api/authentication).
- **Tenancy.** Resolve a tenant with `API::tenant()` and scope criteria with endpoint callbacks. See [API tenancy](/docs/api/tenancy).
- **Caching.** Opt in to ETags with the `ApiCache` middleware. See [API caching](/docs/api/caching).
## Learn more
- [API introduction](/docs/api/introduction)
- [Routes reference](/docs/api/routes)
- [Query parameters](/docs/api/query-parameters)
- [Custom interfaces](/docs/api/custom-interfaces)
- [Custom endpoints](/docs/api/custom-endpoints)
- [OpenAPI / Swagger](/docs/api/openapi)
- [JavaScript client](/docs/client/introduction)
For product paths, see [Use cases — Headless API](/docs/use-cases#headless-api-only).
---
# Guides: Advanced: UI
> Control panels and interface generation for Streams.
Source: https://streams.dev/docs/ui
## When to use Streams UI
Add `streams/ui` when your team needs an admin or product panel—forms, tables, navigation, and pages—built with Livewire and PHP builders on top of Core streams.
You keep Laravel auth, policies, and middleware. Resources are PHP classes—not Blade helpers like `UI::form()`.
## Installation
```bash
composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev
```
Register a panel in a service provider:
```php
UI::panel(
Panel::make('admin')->default()->path('admin')->middleware(['web'])
);
```
## Typical workflow
1. Define streams in `streams/` (Core).
2. Create Resource classes with `form()` and `table()` builders.
3. Register resources on the panel and visit `/admin`.
## Learn more
- [UI introduction](/docs/ui/introduction)
- [Quick start](/docs/ui/quick-start)
- [Panels](/docs/ui/panels)
- [Resources](/docs/ui/resources)
- [Forms](/docs/ui/forms)
- [Tables](/docs/ui/tables)
- [Control panel guide](/docs/control-panel)
See [Use cases](/docs/use-cases) for admin and product panel paths.
---
# Guides: Advanced: Control Panel
> Building admin and product panels with Streams UI.
Source: https://streams.dev/docs/control-panel
## Overview
A control panel (CP) is the authenticated area where your team or customers manage stream data. Streams UI registers panel routes, navigation, and resources from configuration and PHP panel builders.
## Admin vs product panel
| Type | Audience | Example |
|------|----------|---------|
| Admin panel | Internal ops | Manage users, orders, content |
| Product panel | End customers | Account settings, project config |
Same UI package; different panel registration and auth.
## Steps
1. Require `streams/ui` and configure middleware (typically `web`, `auth`).
2. Define a panel with path prefix (e.g. `/admin`).
3. Attach stream resources—tables and forms for each stream.
4. Customize navigation groups and pages as needed.
## Learn more
- [UI panels](/docs/ui/panels)
- [SDK Livewire generator](/docs/sdk/commands#streamslivewire)
- [Use cases](/docs/use-cases)
---
# Guides: Advanced: Users
> Authentication and user streams in Streams applications.
Source: https://streams.dev/docs/users
## Overview
Streams does not replace Laravel authentication. Use Laravel's user model, sessions, Sanctum, or Passport as your team already does.
Define a `users` stream when you want Streams repositories, CP resources, or API exposure for user records.
## Typical setup
- Laravel `User` model for auth
- Optional `users` stream with Eloquent source pointing at the same table
- UI panel resources for admin user management
- API routes only if you intentionally expose user endpoints
## Learn more
- [Streams Core — entries](/docs/core/entries)
- [UI panels](/docs/ui/panels)
- [API authentication](/docs/api/authentication)
---
# Guides: Advanced: Databases
> Storage adapters and database-backed streams.
Source: https://streams.dev/docs/databases
## Overview
Streams is data-source agnostic. Configure each stream's `source` to use filebase files, Eloquent models, in-memory collections, or remote APIs.
Most production apps use Eloquent for transactional data and filebase for content or config-like entries.
## Eloquent source
Point a stream at an existing model:
```json
{
"config": {
"source": {
"type": "eloquent",
"model": "App\\Models\\Post"
}
}
}
```
Your team keeps migrations and models in Laravel; Streams adds repositories, criteria, and optional UI/API layers.
## Filebase source
Store entries as JSON, YAML, Markdown, or HTML under `streams/data/`. Ideal for content sites and git-reviewed data.
## Learn more
- [Streams Core — streams](/docs/core/streams)
- [Repositories](/docs/core/repositories)
- [Files](/docs/files)
---
# Guides: Advanced: Localization
> Internationalization with Streams and Laravel.
Source: https://streams.dev/docs/localization
## Overview
Use Laravel's localization for application strings (`lang/`, `__()`, `@lang`). Streams field labels and CP text can pull from translation files like any Blade view.
For multi-locale **content**, model locales as separate streams, localized fields, or related translation entries— whichever fits your team's CMS pattern.
## Laravel strings
Publish UI lang files when customizing control panel copy:
```bash
php artisan vendor:publish --provider="Streams\\Ui\\UiServiceProvider" --tag=lang
```
## Learn more
- [Laravel localization](https://laravel.com/docs/localization)
- [UI introduction](/docs/ui/introduction)
- [Content modeling](/docs/content)
---
# Guides: Advanced: Tenancy
> Three separate mechanisms: Core applications matched by URL, an API tenant resolver you scope yourself, and a UI panel tenant that does not scope routes yet.
Source: https://streams.dev/docs/tenancy
Streams does not ship one tenancy product. Three packages each hold a piece, and none of them filters data unless you do.
| Need | Use |
|------|-----|
| Different config, streams, or locale per host | Core [applications](/docs/core/applications) |
| One tenant value per API request, then scope queries yourself | [API tenancy](/docs/api/tenancy) |
| A tenant value available to a panel | UI `Panel::tenant()` — stored only, see below |
## Applications
An application is an entry on the stream `config('streams.core.applications_id')` (default `applications`). Core loads that stream at boot, picks one entry by matching `match` patterns against the full request URL (`Str::is`), and activates it. If nothing matches, it uses the first entry with an empty `match`. If the stream is empty, it activates a synthetic `default` application.
`StreamsServiceProvider::bootApplication()` then passes these attributes to `Integrator::integrate()` when they are non-empty:
- `locale` — `App::setLocale()`
- `config` — merged with `Config::set()` after dotting the array
- `aliases`, `bindings`, `singletons` — container registrations
- `streams` — each value is a file path passed to `Streams::load()`, or an array passed to `Streams::register()`
The shipped `applications` stream schema only declares `handle`, `match`, and `config`. The other keys are still read off the entry when the JSON file contains them.
`Integrator` can also merge `routes`, `assets`, `commands`, `listeners`, `policies`, `middleware`, `providers`, `schedules`, and `includes`. Boot does **not** pass those. Putting `routes` on an application entry does nothing until you call `Integrator::integrate()` yourself.
```json
{
"id": "spanish",
"match": ["https://es.example.com/*"],
"locale": "es",
"config": {
"app": {"name": "Corrientes"},
"streams": {"core": {"data_path": "streams/data/es"}}
}
}
```
`match` is compared with `Str::is` against `Request::fullUrl()`, which includes the scheme. A pattern of `es.example.com/*` does not match `https://es.example.com/…`. The example in Core's `applications.json` omits the scheme; include it.
`Integrator::config()` flattens the array with `Arr::dot`, runs string values that contain `}` through `Str::parse`, then `Config::set()` on the dotted keys. Nested config in the file is the right shape. A flat key that already contains a dot, such as `"app.name"`, is also set as that config path.
```php
use Streams\Core\Support\Facades\Applications;
Applications::active(); // the entry chosen for this request
Applications::activate($other); // replace it; does not re-run Integrator
```
Calling `activate()` later does not merge that entry's config again. Integration runs once during boot.
## API
`API::tenant()` and `ApiInterface::tenant()` store a resolver. `API::getTenant()` calls it once per request and binds the result as `streams.api.tenant`. List and show endpoints do not apply it. You listen for their `apply` callback, or you enforce the tenant in gate middleware. Create, update, delete, and query are not covered by that callback. The working pattern is on [API tenancy](/docs/api/tenancy).
## UI panels
`Panel::tenant($value)` stores a value or closure on the panel, and `Streams\Ui\UiManager::tenant()` stores a closure whose `getTenant()` result is available during the request. Resource and page route code that would put that tenant into route parameters is commented out, so a panel tenant does not change URLs or scope entry queries.
Use applications or the API resolver when you need isolation. Use the panel method only when your own page code reads `UI::getTenant()` or `$panel->getTenant()`.
## Related
- [Applications](/docs/core/applications)
- [Integrator](/docs/core/integrator)
- [API tenancy](/docs/api/tenancy)
- [Authentication](/docs/api/authentication)
---
# Guides: Developers: Caching
> Caching options and automation.
Source: https://streams.dev/docs/caching
## Introduction
Streams Core provides a convenient API to link [Laravel cache](https://laravel.com/docs/cache) data to a Stream. When caching in this way, you can flush all cached items related to a Stream together.
### Configuration
In `streams/examples.json`:
```json
{
"config": {
"cache": {
"enabled": true,
"store": "default",
"ttl": 3600
}
}
}
```
## Cache Usage
#### The Cache Instance
To obtain a Stream-linked cache instance, you may use the `cache()` method on the desired Stream instance:
```php
$cache = Streams::make('examples')->cache();
$cache->get('key');
```
### Retrieving Items
Use the `get` method to retrieve items from the cache. If the item does not exist in the cache, `null` will be returned. You may pass a second argument specifying the default value to return if the item doesn't exist:
```php
$cache = Streams::make('examples')->cache();
$value = $cache->get('key');
$value = $cache->get('key', 'default');
```
You may also pass a `closure` as the default value. The get method will return the closure result if the specified item does not exist in the cache. Using a closure allows you to defer the retrieval of expensive default values until they are needed:
```php
$stream = Streams::make('examples');
$value = $stream->cache()->get('key', function () use ($stream) {
return $stream->entries()->all();
});
```
#### Checking Items
Use the `exists` method to check if an item exists in cache:
```php
if (Streams::make('examples')->cache()->has('key')) {
// We have it!
}
```
#### Incrementing/Decrementing Values
Use the `increment` and `decrement` methods to increment or decrement the value of cached integer value:
```php
$cache = Streams::make('examples')->cache();
$cache->increment('key');
$cache->increment('key', $amount);
$cache->decrement('key');
$cache->decrement('key', $amount);
```
#### Retrieve & Store
Use the `remember` method to retrieve an item from the cache and store a default value if the requested item doesn't exist.
```php
$value = Streams::make('examples')->cache()->remember('key', $seconds, function () {
return Streams::entries('examples')->get();
});
```
#### Retrieve & Delete
Use the `pull` method to retrieve an item from the cache and then delete the item.
```php
$value = Streams::make('examples')->cache()->pull('key');
```
### Storing Items
Use the `put` method to store items in the cache:
```php
Streams::make('examples')->cache()->put('key', 'value', $seconds);
```
You can also pass a `DateTime` instance instead of seconds:
```php
Streams::make('examples')->cache()->put('key', 'value', now()->addMinutes(10));
```
If the storage time is not passed to the put method, the item will be stored indefinitely:
```php
Streams::make('examples')->cache()->put('key', 'value');
```
#### Store If Not Present
Use the `add` method to store items in the cache only if they do not already exist:
```php
Streams::make('examples')->cache()->add('key', 'value', $seconds);
```
#### Store Forever
Use the `forever` method to store items in the cache indefinitely:
```php
Streams::make('examples')->cache()->forever('key', 'value');
```
> Items that are stored "forever" may be removed under some circumstances.
### Removing Items
Use the `forget` method to remove items from the cache:
```php
Streams::make('examples')->cache()->forget('key');
```
You can also remove items by providing a zero or negative number of seconds:
```php
Streams::make('examples')->cache()->put('key', 'value', 0);
Streams::make('examples')->cache()->put('key', 'value', -5);
```
> Stream entry changes automatically forget/flush cache items.
#### Removing All Items
You can clear the entire cache using the `flush` method:
```php
Streams::make('examples')->cache()->flush();
```
> The flush method only flushes linked cache.
## Related
- [Core caching reference](/docs/core/caching)
- [Core criteria](/docs/core/criteria)
- [Laravel Cache](https://laravel.com/docs/cache)
---
# Guides: Developers: Addons
> Composing packages and local development with Streams.
Source: https://streams.dev/docs/addons
## Overview
Streams applications compose Composer packages. Your project may require Core only, or Core plus UI, API, SDK, and community addons.
Browse published packages on [Addons](/addons).
## Path repositories
For local development across packages, add path repositories in `composer.json`:
```json
{
"repositories": [
{
"type": "path",
"url": "../streams-core",
"options": { "symlink": true }
}
]
}
```
Prefer `"preferred-install": { "streams/*": "source" }` when contributing to Streams packages.
## Documentation
All package docs live on this site under `/docs/core`, `/docs/ui`, etc. Edit markdown in `streams/data/` in the streams.dev repository.
## Learn more
- [Architecture](/docs/architecture)
- [Installation](/docs/installation)
- [Packages catalog](/addons)
---
# Guides: Developers: Testing
> Testing Streams applications and packages.
Source: https://streams.dev/docs/testing
## Overview
`streams/testing` provides a pre-configured Orchestra Testbench environment, sample streams, and helpers for testing Streams behavior in your app or package.
## Installation
```bash
composer require --dev streams/testing:1.0.x-dev
```
Extend the package TestCase in your PHPUnit tests and use sample stream data for realistic scenarios.
## Learn more
- [Testing introduction](/docs/testing/introduction)
- [Writing tests](/docs/testing/writing-tests)
- [Test data](/docs/testing/test-data)
- [Troubleshooting](/docs/testing/troubleshooting)
Laravel's own [testing documentation](https://laravel.com/docs/testing) applies for HTTP, database, and feature tests outside Streams specifics.
---
# Guides: Developers: Agents
> How agents work with Streams: llms.txt and raw markdown, the MCP server, the stream definition schema, and OpenAPI.
Source: https://streams.dev/docs/agents
Streams docs are meant to be read by coding agents as well as people. This site is the source of truth. Package repositories do not keep a second copy.
## Read the docs
| URL | What you get |
|-----|----------------|
| [/llms.txt](/llms.txt) | An index of every docs page, as markdown links, following [llmstxt.org](https://llmstxt.org). Built on each request from the same index as search. |
| [/llms-full.txt](/llms-full.txt) | The same pages, inlined. Use this when you need the text and cannot fetch each link. |
| `/docs/{id}.md` and `/docs/{package}/{id}.md` | The source of one page, with its title as a heading. Drop `.md` for the HTML page. |
| [/search/docs.json](/search/docs.json) | The Cmd+K index: title, description, URL, markdown URL, and an excerpt. |
| [/docs/api/openapi.yaml](/docs/api/openapi.yaml) | The generic OpenAPI 3 description of `streams/api`, copied from the package. It is not generated from your app's streams. |
| [/schema/streams.schema.json](/schema/streams.schema.json) | The JSON Schema for `streams/*.json` definition files. See [Stream definition schema](/docs/sdk/stream-schema). |
HTML pages also emit `` pointing at the `.md` URL.
These routes are dynamic. They read the docs streams through `App\Support\DocsSearchIndex` and cache the text for 15 minutes (`docs.llms.index`, `docs.llms.full`, `docs.search.index`). Adding a markdown file under `streams/data/{docs,core_docs,ui_docs,api_docs,sdk_docs,testing_docs,client_docs}/` publishes it on the next cache miss. There is no generate step.
## What to trust
Documented behavior on a page marked `status: ready` was checked against package source. If a page and the code disagree, the code wins, and the page should be corrected here.
Two names stay distinct until a later rename lands:
- **SDK** (`streams/sdk`) is the PHP Artisan generators and the local [MCP server](/docs/mcp). See the [command reference](/docs/sdk/commands).
- **Client** (`@laravel-streams/api-client`) is the JavaScript client for the REST API.
## Work inside an app
Inside a Laravel app with `streams/sdk`, an agent doesn't have to read source files to learn the domain model:
- **MCP server.** `php artisan mcp:start streams` exposes 14 tools (list and describe streams, entry and definition schemas, validate definitions, entry CRUD, docs search, and the `make-stream` and `make-addon` generators), two resources, and the `design-stream` prompt. It needs `laravel/mcp`, which needs Laravel 11.45+ or 12.41+. See [MCP server](/docs/mcp).
- **CLI with JSON output.** On any supported Laravel version, `php artisan streams:list --json` lists the streams and `php artisan streams:validate --json` checks definition files. See the [command reference](/docs/sdk/commands).
- **Definition schema.** `make:stream` writes `"$schema": "https://streams.dev/schema/streams.schema.json"`, and this site serves that file at [/schema/streams.schema.json](/schema/streams.schema.json). `streams:schema` writes a schema for the *entries* of each of your streams; that is a different file.
## Related
- [MCP server](/docs/mcp)
- [Workflows](/docs/workflows)
- [Command reference](/docs/sdk/commands)
- [Contributing documentation](/docs/contributing-docs)
---
# Guides: Developers: MCP server
> The local-development MCP server in streams/sdk: requirements, starting it with mcp:start streams, client setup, tools, resources, the design-stream prompt, and configuration.
Source: https://streams.dev/docs/mcp
The SDK includes a local-development [Model Context Protocol](https://modelcontextprotocol.io) server. MCP is the standard way AI agents call tools. With it, an agent working in your app can see the domain model (streams), read and write entries, check the stream definitions it writes, search the Streams docs, and run the SDK generators, all without guessing from source files.
The server runs over stdio on your machine through Artisan. It is not an HTTP endpoint.
## Requirements
- `streams/sdk` installed as a dev dependency (`composer require --dev streams/sdk:1.0.x-dev`).
- [`laravel/mcp`](https://github.com/laravel/mcp) `^1.0`, which needs Laravel 11.45+ or 12.41+.
```bash
composer require --dev laravel/mcp
```
On Laravel 10 the MCP server is not available and is skipped automatically. Agents can still use the CLI equivalents: `php artisan streams:list --json` and `php artisan streams:validate --json`. See the [version matrix](/docs/installation#version-matrix).
## Start the server
```bash
php artisan mcp:start streams
```
You won't normally run this yourself. Your agent client starts it. To try the tools interactively, use the MCP Inspector that ships with `laravel/mcp`:
```bash
php artisan mcp:inspector streams
```
## Connect an agent
Register the server with each client from the app root. Use an absolute path to `artisan` if the client does not start servers in the project directory.
**Claude Code** (writes to `.mcp.json` with `--scope project`, so the team shares it):
```bash
claude mcp add --scope project streams -- php artisan mcp:start streams
```
Or add it to `.mcp.json` yourself:
```json
{
"mcpServers": {
"streams": {
"command": "php",
"args": ["artisan", "mcp:start", "streams"]
}
}
}
```
**Cursor** (`.cursor/mcp.json`) uses the same `mcpServers` shape as `.mcp.json`.
**VS Code** (`.vscode/mcp.json`):
```json
{
"servers": {
"streams": {
"type": "stdio",
"command": "php",
"args": ["artisan", "mcp:start", "streams"]
}
}
}
```
**Codex** (`~/.codex/config.toml`):
```toml
[mcp_servers.streams]
command = "php"
args = ["/absolute/path/to/app/artisan", "mcp:start", "streams"]
```
## Tools
| Tool | What it does | CLI equivalent |
|------|--------------|----------------|
| `list-streams` | Registered streams with ID, name, description, source, and field handles. | `streams:list --json` |
| `describe-stream` | One stream's source, key, routes, definition file, original definition, and fields with resolved rules and config. | |
| `entry-schema` | JSON Schema for a stream's entries. | `streams:schema` |
| `definition-schema` | JSON Schema for `streams/*.json` definition files. | |
| `validate-stream-definition` | Validate a draft definition, `streams/{id}.json`, or every definition. | `streams:validate --json` |
| `list-entries` | Query entries with `where` constraints, ordering, and pagination. | |
| `read-entry` | Read an entry by key. | |
| `create-entry` | Create a validated entry. Fails if the key exists. | `make:entry` |
| `update-entry` | Update some attributes; the merged entry is validated. | |
| `delete-entry` | Delete an entry by key. Marked destructive. | |
| `search-docs` | Keyword search over local Streams docs. | |
| `read-doc` | Read a doc returned by `search-docs`. | |
| `make-stream` | Write a validated definition to `streams/{id}.json` and register it. | `make:stream {id} [--force]` |
| `make-addon` | Scaffold an addon package in `addons/{vendor}/{name}`. | `make:addon` |
Tools annotate themselves as read-only, idempotent, or destructive, so clients can auto-approve safe calls and ask before writes.
Protected fields (`"protected": true`) are left out of entry output.
### Resources and prompts
- `streams://schemas/streams.schema.json`: the [stream definition JSON Schema](/docs/sdk/stream-schema) (also served at [/schema/streams.schema.json](/schema/streams.schema.json)).
- `streams://streams/{id}`: a stream's description (same as `describe-stream`).
- `design-stream` prompt: walks an agent through modeling a new stream (inspect, draft, validate, create, seed).
## Docs search
`search-docs` indexes markdown under these paths, relative to the app root:
- `docs`
- `vendor/streams/*/docs` (docs shipped with installed Streams packages)
- `streams/data/*docs` (docs streams, as on streams.dev)
Change the list with `streams.sdk.mcp.docs.paths`. The index is built from local files only; the server makes no network requests. To search these docs from outside an app, use [/llms.txt](/llms.txt) and the raw `.md` pages described in [Agents](/docs/agents).
## Configuration
Publish the config to change defaults:
```bash
php artisan vendor:publish --tag=streams-sdk-config
```
| Key | Env | Default | Description |
|-----|-----|---------|-------------|
| `streams.sdk.mcp.enabled` | `STREAMS_MCP_ENABLED` | `true` | Register the server. |
| `streams.sdk.mcp.allow_production` | `STREAMS_MCP_ALLOW_PRODUCTION` | `false` | Also register when `APP_ENV=production`. |
| `streams.sdk.mcp.handle` | `STREAMS_MCP_HANDLE` | `streams` | Handle passed to `mcp:start`. |
| `streams.sdk.mcp.read_only` | `STREAMS_MCP_READ_ONLY` | `false` | Hide `create-entry`, `update-entry`, `delete-entry`, `make-stream`, and `make-addon`. |
| `streams.sdk.mcp.docs.paths` | | see above | Glob patterns for docs search. |
## Safety
- The server is for local development. It is not registered in production unless you opt in, and the SDK should be a dev dependency anyway.
- It acts with the full permissions of your app. Entry tools write to whatever source a stream uses (flat files in `streams/data` by default, or your database).
- Flat-file writes land in your working tree, so review them with `git diff` like any other change.
- Use `STREAMS_MCP_READ_ONLY=true` when you only want an agent to look.
## Troubleshooting
- **"MCP Server with name [streams] not found"**: `laravel/mcp` is missing, the app is in production, or `STREAMS_MCP_ENABLED=false`.
- **The client reports invalid JSON**: something in the app wrote to stdout during boot (for example `echo`, `dump`, or `dd` in a service provider). stdout carries the protocol, so send debugging output to the log.
- **A new stream is missing**: streams are registered at boot. `make-stream` registers what it creates, but if you add a definition by hand, restart the server (most clients have a reconnect command).
## Related
- [Agents](/docs/agents)
- [Command reference](/docs/sdk/commands)
- [Stream definition schema](/docs/sdk/stream-schema)
- [Workflows](/docs/workflows)
---
# Guides: Developers: Docs MCP server
> Connect Cursor, Claude Code, or any MCP client to the hosted streams.dev docs over MCP: search, read pages, browse the navigation, and fetch the stream JSON Schema.
Source: https://streams.dev/docs/docs-mcp
streams.dev runs a public, read-only [Model Context Protocol](https://modelcontextprotocol.io) server over this documentation. Point your agent at it and it can search the docs, read whole pages as markdown, and fetch the stream JSON Schema without scraping HTML.
This server is hosted by streams.dev and only reads these docs. It is separate from the SDK's local [MCP server](/docs/mcp) (`php artisan mcp:start streams`), which runs inside your own app and works with its streams and entries. Use both: the SDK server for your app, this one for the docs.
## Endpoint
```text
https://streams.dev/mcp
```
Streamable HTTP transport (JSON-RPC over `POST`), no authentication. Requests are rate limited to 60 per minute per IP; over the limit the server answers `429` with a `Retry-After` header.
## Connect
### Cursor
Add the server to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project):
```json
{
"mcpServers": {
"streams-docs": {
"url": "https://streams.dev/mcp"
}
}
}
```
### Claude Code
```bash
claude mcp add --transport http streams https://streams.dev/mcp
```
### Other clients
Any client that speaks MCP over Streamable HTTP can use the URL above. To try it by hand, run the MCP Inspector:
```bash
npx @modelcontextprotocol/inspector --cli https://streams.dev/mcp --transport http --method tools/list
```
### Local, over stdio
In a clone of the streams.dev repository the same server runs as an Artisan command, reading the docs from your checkout:
```bash
php artisan mcp:start streams-docs
```
```json
{
"mcpServers": {
"streams-docs-local": {
"command": "php",
"args": ["artisan", "mcp:start", "streams-docs"],
"cwd": "/path/to/streams.dev"
}
}
}
```
## Tools
All tools are read-only.
| Tool | Arguments | Returns |
|------|-----------|---------|
| `search_docs` | `query` (required), `package`, `section`, `limit` (default 10, max 50) | Ranked pages with title, slug, URL, markdown URL, package, section, tags, and a snippet |
| `get_page` | `page`: a slug (`core/introduction`), docs path, or URL | The page as markdown with a frontmatter block (title, description, slug, url) |
| `list_pages` | `package`, `section` | The navigation: guides by group, then each package, with title, slug, URL, and section |
| `get_schema` | `name` (default `streams`) | The JSON Schema served at [/schema/streams.schema.json](/schema/streams.schema.json) |
`package` is one of `guides`, `core`, `ui`, `api`, `sdk`, `testing`, `client`. `section` is the page's frontmatter section: `get-started`, `guides`, `concepts`, `reference`, `packages` (every package reference page), or `contributing`. A page's slug is its path under `/docs/`: `installation` for a hub guide, `core/introduction` for a package page.
Pages are also exposed as resources: `streams-docs://pages/{package}/{page}` (for example `streams-docs://pages/core/introduction`), plus `streams-docs://llms.txt` for the index.
## Other ways to read the docs
- [/llms.txt](/llms.txt) and [/llms-full.txt](/llms-full.txt)
- Any docs page with `.md` appended, such as [/docs/core/streams.md](/docs/core/streams.md)
- [/search/docs.json](/search/docs.json)
## Related
- [Agents](/docs/agents)
- [MCP server](/docs/mcp)
---
# Guides: Developers: Workflows
> Streams\Core\Support\Workflow runs named steps in order and fires before and after callbacks.
Source: https://streams.dev/docs/workflows
`Streams\Core\Support\Workflow` is a small ordered step runner in Core. It is not a job pipeline, a queue workflow, or an agent workflow. The Core README still lists replacing it with Laravel pipelines as an open question, so treat it as a stable class with an undecided future, not as a product surface you should build a framework on.
Nothing in Core, UI, or the API dispatches a workflow for you. You subclass it when you want named steps and callbacks in your own code.
## Run steps
Subclass `Workflow`, set `$steps`, and call `process()`. Each step is resolved through the container. A class name uses `handle`. A `Class@method` string or a `[Class::class, 'method']` pair calls that method.
```php
use Streams\Core\Support\Workflow;
class PublishPost extends Workflow
{
public array $steps = [
'validate' => ValidatePost::class,
'notify' => NotifySubscribers::class.'@handle',
];
}
(new PublishPost)->process(['post' => $post]);
```
`process()` returns `void`. It stores the array on `$workflow->payload` and passes that array to `App::call()` as named arguments, so a step method receives values whose parameter names match the array keys. It does not merge return values back into the payload. Share mutable state by passing an object.
For each step named `validate`, the runner fires `before_validate`, runs the step, then fires `after_validate`.
## Add steps
```php
$workflow->addStep('archive', ArchivePost::class); // append
$workflow->doFirst('guard', GuardPost::class); // position 0
$workflow->doBefore('notify', 'audit', AuditPost::class); // before an existing name
$workflow->doAfter('validate', 'stamp', StampPost::class); // after an existing name
```
`doBefore` and `doAfter` look the target up with `array_search` and pass that position to `addStep()`, which type-hints `?int`. A missing target makes `array_search` return `false`, and the call throws `TypeError`. Check that the target name exists.
## Callbacks
`Workflow` uses Core's `FiresCallbacks` trait.
```php
$workflow->addCallback('before_validate', function ($post) {
// $post is the payload value whose key matches this parameter
});
```
`fire()` also calls a method on the workflow when one exists. The method name is `on` plus the callback name in camel case, so `before_validate` looks for `onBeforeValidate`.
`passThrough($object)` forwards each callback to that object. The object's matching `on…` method runs first:
```php
$workflow->passThrough($listener); // $listener->onBeforeValidate($post)
```
Global listeners use `Workflow::addCallbackListener('before_validate', $callback)`. The listener key is the class name plus the callback name, so register it on your subclass, not on `Workflow`, if you want it scoped to that subclass.
## Related
- [Callbacks](/docs/core/callbacks)
- [Agents](/docs/agents)
- [Extending Core](/docs/core/extending-core)
---
# Core reference: Introduction
> JSON-defined streams, repositories, criteria, and Laravel integration.
Source: https://streams.dev/docs/core/introduction
Streams Core (`streams/core`) is the foundation package for the Streams ecosystem. It provides JSON-defined domain models, repository access, criteria-based queries, stream routing, assets, and images — all integrated with Laravel.
## When to read this page
Start here if you are adding `streams/core` to a Laravel project or need an overview before diving into streams, entries, and repositories.
## Core concepts
| Concept | Class / API | Purpose |
|---------|-------------|---------|
| Stream | `Streams\Core\Stream\Stream` | Domain model defined in JSON |
| Entry | `Streams\Core\Entry\Entry` | Single record in a stream |
| Repository | `Streams::repository()` | CRUD access to entries |
| Criteria | `Streams::entries()` | Query builder for entries |
Streams live in `streams/*.json`. Entry data lives in the configured source (filebase, database, self, etc.).
## Minimal example
```json
{
"id": "posts",
"fields": [
{ "handle": "title", "type": "string", "required": true },
{ "handle": "body", "type": "string" }
]
}
```
```php
$post = Streams::repository('posts')->create(['title' => 'Hello']);
$post->save();
$posts = Streams::entries('posts')->where('title', 'Hello')->get();
```
## Package boundaries
Core does not include admin UI (see [UI](/docs/ui/introduction)) or REST endpoints (see [API](/docs/api/introduction)). Validation is opt-in — call `$entry->validator()->validate()` before persisting when you need it.
## Related
- [Installation](/docs/core/installation)
- [Streams](/docs/core/streams)
- [Repositories](/docs/core/repositories)
- [Criteria](/docs/core/criteria)
---
# Core reference: Installation
> Composer, publish config/streams, env vars, and first stream.
Source: https://streams.dev/docs/core/installation
Install Streams Core in a Laravel 10, 11, or 12 application via Composer. Core declares `php: ^8.2` and `laravel/framework ^10|^11|^12`. This site runs Core on Laravel 12. Core's own test suite still runs on Laravel 10, because the `streams/testing` harness is Laravel 10 only until it is widened. See the [version matrix](/docs/installation#version-matrix).
## Require the package
```bash
composer require streams/core:2.0.x-dev
```
Core 2.0 has no stable tag yet. Without the `2.0.x-dev` constraint (or `"minimum-stability": "dev"` in your `composer.json`), Composer installs the old Core 1.10.4 instead.
Laravel auto-discovers `Streams\Core\StreamsServiceProvider`.
## Publish assets
Nothing has to be published; Core runs on its defaults. Publish what you want to change:
```bash
php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=config
php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=streams
php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=public
```
| Tag | Copies | To |
|-----|--------|----|
| `config` | `resources/config/core.php` | `config/streams/core.php` |
| `streams` | Core's own stream definitions (`resources/streams/`: `streams.json`, `applications.json`) | `streams/` |
| `public` | `resources/public/` | `public/vendor/streams/core` |
Always pass `--provider`. `streams/ui` and `streams/api` register a `config` tag too, so `--tag=config` on its own publishes every package's config. The `public` tag publishes nothing when the installed Core build ships no `resources/public` directory.
## Environment variables
| Variable | Purpose |
|----------|---------|
| `STREAMS_DATA_PATH` | Filebase data directory (default `streams/data`) |
| `STREAMS_SOURCE` | Default source adapter (default `filebase`) |
| `STREAMS_DEFAULT_FORMAT` | Default file format (default `json`) |
## Verify installation
Create `streams/posts.json` and query entries:
```php
Streams::entries('posts')->count();
```
## Related
- [Configuration](/docs/core/configuration)
- [Streams](/docs/core/streams)
- [Hub: Installation](/docs/installation)
---
# Core reference: Configuration
> config/streams/core.php — data path, sources, field types, and images.
Source: https://streams.dev/docs/core/configuration
Core configuration lives in `config/streams/core.php`, published from the package. Environment variables override defaults for deployment-specific paths and sources.
## Key settings
| Key | Env | Default | Purpose |
|-----|-----|---------|---------|
| `streams_id` | — | `streams` | Meta-stream storing stream definitions |
| `applications_id` | — | `applications` | Multi-app configuration stream |
| `data_path` | `STREAMS_DATA_PATH` | `streams/data` | Filebase entry directory |
| `default_source` | `STREAMS_SOURCE` | `filebase` | Default adapter when omitted in stream JSON |
| `sources.filebase.default_format` | `STREAMS_DEFAULT_FORMAT` | `json` | Filebase format when a stream sets none |
| `auto_alt` | `STREAMS_AUTO_ALT` | `true` | Generate image alt text when missing |
| `version_images` | `STREAMS_VERSION_IMAGES` | `true` | Append cache-busting version to image URLs |
## OpenSearch connections
The `opensearch` source adapter reads connections from `opensearch`:
| Key | Env | Default |
|-----|-----|---------|
| `opensearch.default` | `OPENSEARCH_CONNECTION` | `default` |
| `opensearch.connections.default.hosts` | `OPENSEARCH_HOST` | `https://localhost:9200` |
| `opensearch.connections.default.username` | `OPENSEARCH_USERNAME` | none |
| `opensearch.connections.default.password` | `OPENSEARCH_PASSWORD` | none |
| `opensearch.connections.default.ssl_verification` | `OPENSEARCH_SSL_VERIFICATION` | `true` |
## Source formats
Under `sources.filebase.formats`, Core registers parsers for filebase entries:
| Format | Class |
|--------|-------|
| `json` | `Json` |
| `yaml` | `Yaml` |
| `html` | `Html` |
| `md` | `Markdown` |
| `tpl` | `Template` |
Set per-stream via `config.source.format`.
## Field types
The `field_types` array maps handle strings to PHP field type classes. Register custom types here when extending Core:
```php
'field_types' => [
'string' => \Streams\Core\Field\Types\StringFieldType::class,
// ...
'my_type' => \App\Streams\MyFieldType::class,
],
```
See [Fields](/docs/core/fields) for registered handles.
## Per-stream config
Individual streams override global defaults in their JSON `config` block:
```json
{
"config": {
"source": { "type": "filebase", "format": "md" },
"cache": { "enabled": true, "ttl": 3600, "store": "redis" }
}
}
```
See [Caching](/docs/core/caching) and [Sources and adapters](/docs/core/sources-and-adapters).
## Related
- [Installation](/docs/core/installation)
- [Streams](/docs/core/streams)
- [Extending Core](/docs/core/extending-core)
---
# Core reference: Streams
> Stream JSON schema — fields, extends, imports, routes, and config.
Source: https://streams.dev/docs/core/streams
A **stream** is a domain model defined in JSON under `streams/`. Core loads definitions at boot and exposes them through the `Streams` facade.
## Stream file structure
```json
{
"id": "posts",
"name": "Posts",
"description": "Blog posts",
"extends": "base_content",
"config": {
"source": {
"type": "filebase",
"format": "json"
},
"cache": { "enabled": false }
},
"fields": [
{ "handle": "title", "type": "string", "required": true },
{ "handle": "body", "type": "string" }
],
"routes": [
{
"uri": "blog/{id}",
"view": "posts.show",
"defer": true
}
]
}
```
| Key | Purpose |
|-----|---------|
| `id` | Stream handle (used in `Streams::make('posts')`) |
| `extends` | Merge fields and config from a parent stream |
| `config` | Source, repository, cache, abstract entry class |
| `fields` | Field definitions (array or shorthand map) |
| `routes` | Stream-managed Laravel routes |
## Field shorthand
Fields can be a map of handle → type string:
```json
{
"fields": {
"title": "string",
"published": "boolean"
}
}
```
Or full objects with `handle`, `type`, `required`, `unique`, `protected`, and `rules`.
## `@` imports
Values starting with `@` load JSON from a project path:
```json
{
"fields": "@streams/fields/common.json"
}
```
Imports resolve with `base_path()` — only JSON files are supported for `@` imports.
## Abstract streams
Set `config.abstract: true` for streams that define shared fields but are not instantiated directly. Child streams use `extends`.
## Meta-stream
Stream definitions themselves are entries in the stream identified by `config('streams.core.streams_id')` (default `streams`). The `streams` stream uses a self or file source depending on your project layout.
## Accessing streams
```php
$stream = Streams::make('posts');
$stream->fields();
$stream->repository();
$stream->entries(); // returns Criteria
```
## Related
- [Fields](/docs/core/fields)
- [Routes](/docs/core/routes)
- [Sources and adapters](/docs/core/sources-and-adapters)
- [Hub: Streams concept](/docs/streams)
---
# Core reference: Fields
> Field handles, rules, shorthands, and registered types from core.php.
Source: https://streams.dev/docs/core/fields
Fields define entry attributes on a stream. Each field has a **handle**, **type**, and optional validation, defaults, and UI input config.
## Definition formats
**Shorthand map:**
```json
{
"fields": {
"title": "string",
"published": "boolean"
}
}
```
**Full object** (list form):
```json
{
"fields": [
{
"handle": "slug",
"type": "slug",
"required": true,
"unique": true,
"rules": ["alpha_dash"],
"protected": false
}
]
}
```
**Import:** the whole `fields` value, or any one field, can be `"@path/to/file.json"`. Core replaces it with the decoded JSON file (relative to the app root) when it builds the stream:
```json
{
"fields": {
"title": "string",
"seo": "@streams/fields/seo.json"
}
}
```
## Common field options
| Option | Purpose |
|--------|---------|
| `required` | Adds required validation rule |
| `unique` | Unique within stream (via StreamsPresenceVerifier) |
| `rules` | Additional Laravel validation rules |
| `protected` | Omit from `toArray()` / `toJson()` |
| `config.default` | Default value via factory |
| `input` | Admin UI hints (used by Streams UI) |
## Registered types
From `config/streams/core.php` `field_types`:
| Category | Types |
|----------|-------|
| Numbers | `number`, `integer`, `decimal` |
| Strings | `string`, `url`, `uuid`, `hash`, `slug`, `email`, `encrypted` |
| Boolean | `boolean` |
| Dates | `datetime`, `date`, `time` |
| Selection | `enum`, `select`, `multiselect` |
| Structured | `array`, `object` |
| Relations | `relationship`, `polymorphic` |
| Media | `file`, `image` |
| Other | `color` |
`enum` is an alias of `select`. There is no `text`, `textarea`, `markdown`, `html`, or `multiple` type. Use `string` with an `input` hint (for example `{"type": "textarea"}`) for long content, `multiselect` for several options, and `"multiple": true` in a relationship's `config` for several related entries.
## Type-specific config
These are the `config` keys Core reads. Put them inside `config`, never next to `type`.
| Type | Key | Meaning |
|------|-----|---------|
| `relationship` | `related` (required) | Related stream ID |
| `relationship` | `multiple`, `key_name`, `relation` | Store a list of keys; related key field (default `id`); relation name for eager loading (default: handle without `_id`) |
| `select`, `enum`, `multiselect` | `options` (required) | Object of value => label, or a list of values |
| `array` | `items` | List of allowed item types, each `{"type": "..."}`; `enforce_items: false` skips the check |
| `array` | `stream`, `related` | Cast items to entries of a stream (ID or inline definition) or look them up by key |
| `array`, `object` | `wrapper` | Class used to wrap the value |
| `object` | `allowed` | List of `{"stream": ...}`, `{"generic": ...}`, or `{"prototype": ...}` objects |
| `polymorphic` | `related` | Optional list of expected stream IDs (not enforced) |
| `uuid` | `default: true` | Generate a UUID when missing |
| `slug` | `separator` | Word separator (default `-`) |
| `decimal` | `precision` | Decimal places |
| `date`, `datetime`, `time` | `format`, `timezone` | Storage format; timezone (default `app.timezone`) |
| `color` | `format` | Default decorator output (`hex`) |
Examples of every type are in [Field types](/docs/fields#field-types). The [stream definition schema](/docs/sdk/stream-schema) checks these shapes.
## Accessing values
```php
$entry->title;
$entry->setAttribute('title', 'Hello');
$entry->decorate('body'); // formatted output
```
## Related
- [Field decorators](/docs/core/field-decorators)
- [Validation](/docs/core/validation)
- [Hub: Fields](/docs/fields)
---
# Core reference: Field decorators
> Decorate entry field values for display — markdown, HTML, and type decorators.
Source: https://streams.dev/docs/core/field-decorators
Field decorators transform raw stored values into display-ready output. Call `$entry->decorate()` to apply type-specific decorators registered on field types.
## When to use decorators
Use decorators in views when you need formatted output (rendered markdown, humanized dates) without mutating the stored value.
## Basic usage
```php
$html = $entry->decorate('body');
```
Each field type may register a decorator that knows how to format its value. String fields with markdown configuration return parsed HTML through the decorator pipeline.
## Protected fields
Fields marked `protected: true` are omitted from `toArray()` and `toJson()` but remain accessible on the entry object and through `decorate()` when authorized.
```json
{ "handle": "internal_notes", "type": "string", "protected": true }
```
## Related
- [Fields](/docs/core/fields)
- [Entries](/docs/core/entries)
- [Views and includes](/docs/core/views-and-includes)
---
# Core reference: Entries
> Entry lifecycle, attributes, factory, save/delete, and protected fields.
Source: https://streams.dev/docs/core/entries
An **entry** is a single record in a stream. Entries are instances of `Streams\Core\Entry\Entry` or a custom class set in stream `config.abstract`.
## Creating entries
**Persist immediately** via repository or criteria:
```php
$post = Streams::repository('posts')->create([
'title' => 'Hello',
]);
// create() calls save() internally
```
**In-memory only** via factory (defaults applied, not saved):
```php
$post = Streams::make('posts')->factory()->create(['title' => 'Draft']);
$post->save();
```
Factory batch helpers: `collect($count)`, `state($callback)`.
## Reading and updating
```php
$post = Streams::entries('posts')->find('my-post');
$post->title = 'Updated';
$post->save();
```
## Deleting
```php
$post->delete();
```
## Validation
Validation is **opt-in**. Core does not validate automatically on save:
```php
$post->validator()->validate();
$post->save();
```
See [Validation](/docs/core/validation).
## Protected fields
Fields with `"protected": true` are excluded from serialization:
```php
$post->toArray(); // omits protected fields
```
## Callbacks
Entry lifecycle hooks use Core callbacks (not Laravel events): `creating`, `created`, `saving`, `saved`, `deleting`. See [Callbacks](/docs/core/callbacks).
## Related
- [Repositories](/docs/core/repositories)
- [Validation](/docs/core/validation)
- [Field decorators](/docs/core/field-decorators)
---
# Core reference: Repositories
> Repository CRUD API — repository() vs entries() and available methods.
Source: https://streams.dev/docs/core/repositories
Repositories provide direct CRUD access to stream entries through `Streams\Core\Repository\Repository`.
## repository() vs entries()
| API | Returns | Use for |
|-----|---------|---------|
| `Streams::repository('posts')` | `Repository` | find, create, save, delete by id |
| `Streams::entries('posts')` | `Criteria` | Query chains (where, orderBy, paginate) |
Both operate on the same underlying adapter. Prefer `entries()` when filtering; prefer `repository()` for single-record CRUD.
```php
$repo = Streams::repository('posts');
$post = $repo->find('hello-world');
Streams::entries('posts')->where('published', true)->get();
```
## Repository methods
| Method | Purpose |
|--------|---------|
| `all()` | All entries |
| `find($id)` | Entry by key or null |
| `findBy($field, $value)` | First match |
| `findAllWhere($field, $value)` | Collection of matches |
| `count()` | Entry count |
| `create($attributes)` | Create and persist |
| `save($entry)` | Persist changes |
| `delete($entry)` | Remove entry |
| `truncate()` | Remove all entries |
| `newInstance($attributes)` | Unsaved entry |
| `newCriteria()` | Fresh criteria instance |
## Not available
These methods do **not** exist on Repository:
- `createMany()` — create entries in a loop or use criteria
- `paginate()` — use `Streams::entries('posts')->paginate()`
- `update()` — mutate the entry object and call `save()`
## Custom repositories
Bind a custom class in stream config:
```json
{
"config": {
"repository": "App\\Streams\\PostRepository"
}
}
```
See [Extending Core](/docs/core/extending-core).
## Related
- [Criteria](/docs/core/criteria)
- [Entries](/docs/core/entries)
---
# Core reference: Criteria
> Query API — where, orderBy, paginate, cache, chunk, and adapter forwarding.
Source: https://streams.dev/docs/core/criteria
`Streams::entries()` returns a `Streams\Core\Criteria\Criteria` instance — a fluent query API executed by the stream's source adapter.
## Basic queries
```php
$posts = Streams::entries('posts')
->where('published', true)
->orderBy('created_at', 'desc')
->get();
$post = Streams::entries('posts')->find('my-slug');
$count = Streams::entries('posts')->where('published', true)->count();
```
## Pagination
```php
$paginator = Streams::entries('posts')->paginate(15);
// Or with options array
$paginator = Streams::entries('posts')->paginate([
'per_page' => 20,
'page' => 2,
]);
```
## Cache
When stream `config.cache.enabled` is true, `get()` and `count()` cache automatically. Override per query:
```php
Streams::entries('posts')->cache(600)->get();
Streams::entries('posts')->fresh()->get(); // bypass cache
```
Mutations (`create`, `save`, `delete`, `truncate`) flush the stream cache.
## Chunk
```php
Streams::entries('posts')->chunk(100, function ($entries, $page) {
foreach ($entries as $entry) {
// process
}
});
```
## Upsert helpers
```php
Streams::entries('posts')->firstOrCreate(['slug' => 'hello'], ['title' => 'Hello']);
Streams::entries('posts')->updateOrCreate(['slug' => 'hello'], ['title' => 'Updated']);
```
## Adapter forwarding
Unknown method calls on Criteria forward to the underlying adapter when supported.
## Related
- [Repositories](/docs/core/repositories)
- [Caching](/docs/core/caching)
- [Sources and adapters](/docs/core/sources-and-adapters)
---
# Core reference: Sources and adapters
> filebase, file, self, database, eloquent, collection, and filesystem adapters.
Source: https://streams.dev/docs/core/sources-and-adapters
Each stream's `config.source.type` selects a **repository adapter** that reads and writes entries. Core resolves the adapter in `Streams\Core\Repository\Repository`.
## Adapter types
| Type | Adapter | Typical config |
|------|---------|----------------|
| `filebase` | `FilebaseAdapter` | `path`, `format` (json, yaml, md, html) |
| `file` | `FileAdapter` | `path` (single file) |
| `self` | `SelfAdapter` | entries in stream JSON `data` array |
| `database` | `DatabaseAdapter` | `table`, `connection` |
| `eloquent` | `EloquentAdapter` | `model` (Eloquent class) |
| `collection` | `CollectionAdapter` | `data` (inline array) |
| `filesystem` | `FilesystemAdapter` | `disk` (Laravel Storage disk) |
| `elasticsearch` | `ElasticsearchAdapter` | `index` (default: stream ID), `search_fields`, `scout_prefix`. Requires `elasticsearch/elasticsearch`. |
| `opensearch` | `OpenSearchAdapter` | `index`, `search_fields`, `scout_prefix`; connections in `streams.core.opensearch`. Requires `opensearch-project/opensearch-php`. |
Default type comes from `config('streams.core.default_source')` (`filebase`).
## Filebase (most common)
```json
{
"config": {
"source": {
"type": "filebase",
"format": "md"
}
}
}
```
Entries live under `config('streams.core.data_path')` in a directory named for the stream handle unless `path` is set.
## Self source
Used when entries are embedded in the stream definition:
```json
{
"config": { "source": { "type": "self" } },
"data": [
{ "id": "one", "title": "First" }
]
}
```
## Eloquent
```json
{
"config": {
"source": {
"type": "eloquent",
"model": "App\\Models\\Post"
}
}
}
```
## Custom adapter
Set `config.adapter` to a fully qualified adapter class, or `config.criteria` for a custom criteria class. See [Extending Core](/docs/core/extending-core).
## Related
- [Streams](/docs/core/streams)
- [Configuration](/docs/core/configuration)
- [Hub: Databases](/docs/databases)
---
# Core reference: Caching
> Per-stream cache config, criteria cache(), fresh(), and flush on writes.
Source: https://streams.dev/docs/core/caching
Core caches criteria results per stream when enabled. Cache keys are prefixed with `streams.{handle}.` via `Streams\Core\Stream\StreamCache`.
## Stream config
```json
{
"config": {
"cache": {
"enabled": true,
"ttl": 3600,
"store": "redis"
}
}
}
```
| Key | Purpose |
|-----|---------|
| `enabled` | Auto-cache `get()` and `count()` on criteria |
| `ttl` | Default seconds for query cache |
| `store` | Laravel cache store name |
## Query cache
```php
// Explicit TTL and optional key
Streams::entries('posts')->cache(600, 'posts.published')->get();
// Bypass cache for one query
Streams::entries('posts')->fresh()->get();
```
## Stream-level remember
```php
Streams::make('posts')->cache()->remember('meta', 3600, fn () => ...);
```
## Flush behavior
These operations flush the stream cache:
- `Criteria::create()`
- `$entry->save()` through criteria/repository
- `$entry->delete()`
- `truncate()`
## Related
- [Criteria](/docs/core/criteria)
- [Hub: Caching](/docs/caching)
---
# Core reference: Validation
> Manual validator() usage, field rules, and StreamsPresenceVerifier.
Source: https://streams.dev/docs/core/validation
Core validation is **opt-in**. Saving an entry does not run validation automatically — call `validate()` when your application requires it.
## Entry validation
```php
$post = Streams::repository('posts')->newInstance([
'title' => '',
]);
$post->validator()->validate(); // throws ValidationException on failure
$post->save();
```
## Stream validator
Build a validator from raw data:
```php
$validator = Streams::make('posts')->validator([
'title' => 'Hello',
], $existingId);
$validator->validate();
```
Rules come from field definitions (`required`, `unique`, custom `rules` arrays) and stream-level `rules`.
## Unique rules
`Streams\Core\Validation\StreamsPresenceVerifier` routes `unique` and `exists` rules through the stream's criteria adapter so uniqueness is checked against stream entries, not arbitrary database tables.
## Field rules
Fields map JSON flags to Laravel rule strings during stream build:
```json
{ "handle": "slug", "type": "slug", "required": true, "unique": true }
```
Add explicit rules:
```json
{ "handle": "title", "type": "string", "rules": ["max:255"] }
```
## Related
- [Entries](/docs/core/entries)
- [Fields](/docs/core/fields)
---
# Core reference: Routes
> Stream routes JSON, Route::streams, EntryController, parse and defer.
Source: https://streams.dev/docs/core/routes
Streams can register Laravel routes from JSON and through the `Route::streams()` macro.
## Stream JSON routes
```json
{
"routes": [
{
"handle": "view",
"uri": "{uri}",
"parse": true,
"view": "{layout}"
},
{
"uri": "blog/{id}",
"view": "posts.show",
"defer": true
}
]
}
```
| Option | Purpose |
|--------|---------|
| `parse` | Register one route per entry (bind `{field}` placeholders from entries) |
| `defer` | Register routes on `App::booted()` instead of immediately |
| `view` | Blade view name passed to `EntryController` |
| `middleware`, `verb`, `csrf` | Standard Laravel route options |
URI placeholders like `{entry.slug}` become criteria bindings (`entry__slug` internally).
## Route::streams macro
```php
Route::streams('posts/{id}', [
'stream' => 'posts',
'view' => 'posts.show',
]);
```
String routes without `@` default to `EntryController` with a `view` parameter.
## EntryController
`Streams\Core\Http\Controller\EntryController` resolves:
- The target stream
- The entry (by route id or criteria parameters)
- The view or redirect from route config
Returns `Response::view()` or a redirect response.
## URL helper
```php
URL::streams('posts/{id}', ['id' => 'hello']);
```
## Related
- [Hub: Routing](/docs/routing)
- [Hub: Site pages](/docs/site-pages)
- [Applications](/docs/core/applications)
---
# Core reference: Applications
> Multi-app URL matching, active application, and Integrator merge keys.
Source: https://streams.dev/docs/core/applications
Applications are entries in the stream identified by `config('streams.core.applications_id')` (default `applications`). They enable multi-tenant or multi-site setups where each application merges its own config and streams at boot.
The site-level picture, including what boot does not merge, is the [Tenancy guide](/docs/tenancy).
## Application entries
The stream schema declares `handle`, `match`, and `config`. Boot also reads these attributes when the entry JSON contains them:
- `match` — URL patterns compared with `Str::is` against `Request::fullUrl()` (include the scheme)
- `locale`
- `config` — merged into Laravel config
- `aliases`, `bindings`, `singletons`
- `streams` — paths passed to `Streams::load()`, or arrays passed to `Streams::register()`
`Integrator` can merge routes, but `bootApplication()` does not pass a `routes` key. Putting `routes` on the entry has no effect at boot.
Core activates the matching application via `Streams\Core\Support\Facades\Applications`.
## Activation flow
1. `StreamsServiceProvider` loads the applications stream
2. `ApplicationManager` matches the incoming request URL
3. `Applications::activate()` sets the active application
4. `Integrator::integrate()` merges the application's configuration
## Accessing the active application
```php
Applications::active();
Applications::activate($applicationEntry);
```
Application entries extend `Streams\Core\Application\Application` (subclass of `Entry`).
## Related
- [Tenancy guide](/docs/tenancy)
- [Integrator](/docs/core/integrator)
- [Routes](/docs/core/routes)
---
# Core reference: Integrator
> Wire application entries into config, streams, routes, and providers at boot.
Source: https://streams.dev/docs/core/integrator
`Streams\Core\Support\Integrator` merges application (or addon) configuration into Laravel during boot.
## integrate()
```php
Integrator::integrate([
'locale' => 'en',
'config' => ['app.name' => 'Acme'],
'streams' => [
'pages' => ['name' => 'Pages', 'fields' => []],
],
'routes' => function ($router) {
// register routes
},
]);
```
## Available merge keys
| Key | Method | Purpose |
|-----|--------|---------|
| `locale` | `Integrator::locale()` | App locale |
| `config` | `Integrator::config()` | Config values |
| `assets` | `Integrator::assets()` | Asset paths |
| `aliases` | `Integrator::aliases()` | Class aliases |
| `bindings` | `Integrator::bindings()` | Container bindings |
| `singletons` | `Integrator::singletons()` | Singleton bindings |
| `commands` | `Integrator::commands()` | Artisan commands |
| `listeners` | `Integrator::listeners()` | Event listeners |
| `policies` | `Integrator::policies()` | Authorization policies |
| `routes` | `Integrator::routes()` | Route closures |
| `providers` | `Integrator::providers()` | Service providers |
| `schedules` | `Integrator::schedules()` | Scheduled tasks |
| `middleware` | `Integrator::middleware()` | Middleware aliases |
| `streams` | `Integrator::streams()` | Stream definitions |
| `includes` | `Integrator::includes()` | View includes |
Applications call `Integrator::integrate()` automatically when activated. Addons use the same mechanism during package boot.
## Related
- [Applications](/docs/core/applications)
- [Addons](/docs/core/addons)
---
# Core reference: Addons
> streams-addon Composer discovery and the Addons facade.
Source: https://streams.dev/docs/core/addons
Addons are Composer packages tagged `streams-addon` that register streams, routes, and config through Core's Integrator at boot.
## Composer discovery
Add to your addon's `composer.json`:
```json
{
"extra": {
"laravel": {
"providers": ["MyVendor\\MyAddon\\ServiceProvider"]
},
"streams": {
"addon": true
}
}
}
```
The addon service provider typically calls `Integrator::integrate()` with streams and bindings.
## Addons facade
```php
use Streams\Core\Support\Facades\Addons;
Addons::register('my-addon', $paths);
```
Use the facade to inspect registered addon paths and assets.
## Related
- [Integrator](/docs/core/integrator)
- [Hub: Addons](/docs/addons)
---
# Core reference: Assets
> Asset registry, path namespaces, @assets Blade, and HTML helpers.
Source: https://streams.dev/docs/core/assets
Core provides an asset registry for grouping CSS, JS, and inline fragments referenced from Blade views. There is no built-in upload pipeline or CDN integration — register paths and emit tags yourself.
## Registering assets
```php
use Streams\Core\Support\Facades\Assets;
Assets::register('theme.css', 'resources/css/theme.css');
Assets::add('styles', 'theme.css');
```
Collections group assets by name (`styles`, `scripts`, etc.).
## HTML helpers
```php
Assets::url('theme.css');
Assets::tag('theme.css');
Assets::script('app.js');
Assets::style('theme.css');
Assets::inline('critical.css');
Assets::contents('public::img/logo.svg');
```
## @assets Blade directive
```blade
@assets('styles', 'theme.css')
@endassets
```
The directive captures the view path and registers inline content into the named collection.
## Path namespaces
UI and addons register namespaces via `Assets::addPath()` — for example `ui` maps to package resources.
## Related
- [Facades and helpers](/docs/core/facades-and-helpers)
- [Hub: Assets](/docs/assets)
---
# Core reference: Images
> Images::make, alterations, picture/srcset, versioning, and auto-alt.
Source: https://streams.dev/docs/core/images
Core image handling wraps Intervention Image with stream-aware URL generation, alterations, and responsive output.
## Creating images
```php
use Streams\Core\Support\Facades\Images;
$url = Images::make('storage://uploads/photo.jpg')
->resize(800, 600)
->crop(400, 400)
->url();
```
`Images::make()` accepts path strings or arrays and detects local, remote, and storage sources.
## Alterations
Alterations chain via `__call()` — common methods include `resize`, `crop`, `fit`, `widen`, `heighten`, `blur`, `greyscale`, and `quality`.
## Responsive output
```php
Images::make('photo.jpg')->picture();
Images::make('photo.jpg')->srcset();
Images::make('photo.jpg')->img();
```
## Configuration
| Key | Purpose |
|-----|---------|
| `streams.core.auto_alt` | Generate alt text when missing |
| `streams.core.version_images` | Append version query for cache busting |
## Related
- [Assets](/docs/core/assets)
- [Hub: Images](/docs/images)
---
# Core reference: Views and includes
> View namespaces, Includes slots, ViewTemplate, and Factory includes.
Source: https://streams.dev/docs/core/views-and-includes
Core extends Laravel views with named include slots and template parsing helpers.
## Includes facade
```php
use Streams\Core\Support\Facades\Includes;
Includes::include('sidebar', 'partials.sidebar');
echo Includes::render('sidebar', ['user' => $user]);
```
| Method | Purpose |
|--------|---------|
| `include($slot, $name, $include?)` | Register an include for a slot |
| `slot($name)` | Get slot contents |
| `render($slot, $payload)` | Render slot with data |
## Factory macros
Blade `@include` parsing extensions via Factory macros:
```php
Factory::parse($template, $data);
Factory::include($name, $data);
Factory::includes($includes);
```
## ViewTemplate
`ViewTemplate` tracks template paths for asset collection registration in `@assets` directives.
## Related
- [Assets](/docs/core/assets)
- [Macros](/docs/core/macros)
---
# Core reference: Facades and helpers
> Streams, Assets, Images, Applications facades and global helpers.
Source: https://streams.dev/docs/core/facades-and-helpers
Core registers facades and global helpers for common stream operations.
## Facades
| Facade | Accessor | Purpose |
|--------|----------|---------|
| `Streams` | `streams` | Stream manager, make, load |
| `Assets` | `assets` | Asset registry |
| `Images` | `images` | Image alterations |
| `Includes` | `includes` | View include slots |
| `Applications` | `applications` | Multi-app activation |
| `Addons` | `addons` | Addon registry |
## Global helpers
Defined in `src/helpers.php`:
```php
stream('posts'); // Stream instance
entries('posts'); // Criteria
repository('posts'); // Repository
html_attributes($attrs); // HTML attribute string
response_time(); // Debug helper
memory_usage(); // Debug helper
```
Prefer `Streams::make()`, `Streams::entries()`, and `Streams::repository()` when facades are not imported.
## Related
- [Repositories](/docs/core/repositories)
- [Assets](/docs/core/assets)
- [Macros](/docs/core/macros)
---
# Core reference: Macros
> Route::streams, Str::parse, Arr::make, Factory includes, and more.
Source: https://streams.dev/docs/core/macros
Core registers macros on Laravel classes during `StreamsServiceProvider::registerMacros()`.
## Route and URL
```php
Route::streams('posts/{id}', ['stream' => 'posts', 'view' => 'posts.show']);
URL::streams('posts/{id}', ['id' => 'hello']);
```
## Str
| Macro | Purpose |
|-------|---------|
| `Str::parse()` | Parse template strings with entry data |
| `Str::purify()` | Sanitize HTML |
| `Str::humanize()` | Human-readable strings |
| `Str::truncate()` | Truncate with ellipsis |
| `Str::isSerialized()` | Detect serialized PHP values |
## Arr
| Macro | Purpose |
|-------|---------|
| `Arr::make()` | Normalize array input |
| `Arr::parse()` | Parse nested structures |
| `Arr::export()` | Export array to string |
| `Arr::htmlAttributes()` | Build HTML attributes |
## Factory (View)
```php
Factory::parse($content, $data);
Factory::include($partial, $data);
Factory::includes($map);
```
## Translator
```php
Translator::translate($key, $replace, $locale);
```
## Related
- [Routes](/docs/core/routes)
- [Views and includes](/docs/core/views-and-includes)
---
# Core reference: Callbacks
> FiresCallbacks on streams, entries, repositories, and criteria.
Source: https://streams.dev/docs/core/callbacks
Core uses the `FiresCallbacks` trait for lifecycle hooks. These are **not** Laravel `Event::` events — they fire synchronously on the object that defines them.
## Trait API
Classes using `Streams\Core\Support\Traits\FiresCallbacks`:
| Method | Purpose |
|--------|---------|
| `addCallback($name, $callback)` | Register callback |
| `addCallbackListener($name, $callback)` | Listen without replacing |
| `observeCallbacks($object)` | Copy callbacks from another object |
| `fire($name, $payload)` | Invoke callbacks |
| `hasCallback($name)` | Check registration |
## Entry lifecycle callbacks
| Callback | When |
|----------|------|
| `creating` | Before create persists |
| `created` | After create persists |
| `saving` | Before save |
| `saved` | After save |
| `deleting` | Before delete |
## Stream callbacks
Streams fire callbacks such as `built` when the stream definition is assembled.
## Example
```php
Streams::make('posts')->addCallbackListener('creating', function ($payload) {
// inspect $payload['entry']
});
```
## Related
- [Entries](/docs/core/entries)
- [Repositories](/docs/core/repositories)
---
# Core reference: Extending Core
> Custom adapters, repositories, entries, and field types via config bindings.
Source: https://streams.dev/docs/core/extending-core
Extend Core by binding custom classes in stream JSON or Laravel service providers.
## Custom field types
Register in `config/streams/core.php`:
```php
'field_types' => [
'rating' => \App\Streams\Fields\RatingFieldType::class,
],
```
Implement a field type class extending Core's field type base with assignment, validation, and optional decorators.
## Custom repository
```json
{
"config": {
"repository": "App\\Streams\\PostRepository"
}
}
```
Implement `Streams\Core\Repository\Contract\RepositoryInterface` or extend `Repository`.
## Custom entry class
```json
{
"config": {
"abstract": "App\\Streams\\PostEntry"
}
}
```
Subclass `Streams\Core\Entry\Entry` for domain methods on entries.
## Custom adapter
```json
{
"config": {
"adapter": "App\\Streams\\Adapters\\ApiAdapter"
}
}
```
Adapters implement criteria execution for your storage backend.
## Custom criteria class
```json
{
"config": {
"criteria": "App\\Streams\\PostCriteria"
}
}
```
## Service provider bindings
Use Laravel's container in `AppServiceProvider` for cross-cutting bindings:
```php
$this->app->bind(CustomInterface::class, CustomImplementation::class);
```
## Related
- [Configuration](/docs/core/configuration)
- [Sources and adapters](/docs/core/sources-and-adapters)
- [Fields](/docs/core/fields)
---
# UI reference: Introduction
> Livewire admin panels on Core — panels, resources, and builders.
Source: https://streams.dev/docs/ui/introduction
Streams UI (`streams/ui`) builds Livewire admin panels on top of Streams Core. You register a **panel**, define **resource** classes for each stream, and configure **forms** and **tables** with PHP builders.
## Architecture
```text
Panel (UI::panel)
└── Resources (PHP classes)
├── ListEntries → Table builder
├── CreateEntry → Form builder
└── EditEntry → Form builder
```
There are no `UI::form()` or `UI::table()` Blade helpers. Builders are PHP objects wired through Livewire pages.
## Minimal setup
```php
use Streams\Ui\Support\Facades\UI;
use Streams\Ui\Builders\Panels\Panel;
UI::panel(
Panel::make('admin')
->default()
->path('admin')
->middleware(['web'])
->resources([PostResource::class])
);
```
Visit `/admin` after registering at least one resource.
## Related
- [Quick start](/docs/ui/quick-start)
- [Installation](/docs/ui/installation)
- [Panels](/docs/ui/panels)
---
# UI reference: Installation
> Composer, UI::panel(Panel::make()), publish tags, and middleware.
Source: https://streams.dev/docs/ui/installation
Install UI alongside Core in your Laravel application.
## Require the package
```bash
composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev
```
UI requires `streams/core ^2.0` and Livewire `^3.0`. Neither Core 2.0 nor UI 1.0 has a stable tag yet, so require both development branches explicitly (or set `"minimum-stability": "dev"` with `"prefer-stable": true`). See [Versions and support](/docs/versions).
## Register a panel
In `AppServiceProvider::boot()`:
```php
use Streams\Ui\Support\Facades\UI;
use Streams\Ui\Builders\Panels\Panel;
UI::panel(
Panel::make('admin')
->default()
->path('admin')
->brandName('My App')
->middleware(['web'])
);
```
## Publish tags
From `UiServiceProvider`:
```bash
php artisan vendor:publish --tag=config --provider="Streams\Ui\UiServiceProvider"
php artisan vendor:publish --tag=laravel-streams --provider="Streams\Ui\UiServiceProvider"
php artisan vendor:publish --tag=ui --provider="Streams\Ui\UiServiceProvider"
```
| Tag | Output |
|-----|--------|
| `config` | `config/streams/ui.php` |
| `laravel-streams` | `streams/` scaffold |
| `ui` | `resources/views/vendor/ui/` view overrides |
Package views use the `ui::` namespace. Assets register via `Assets::addPath('ui', ...)`.
## Middleware
UI registers middleware alias `panel` → `Streams\Ui\Http\Middleware\SetUpPanel`. Panel routes apply `panel:{id}` automatically.
## Related
- [Quick start](/docs/ui/quick-start)
- [Panels](/docs/ui/panels)
---
# UI reference: Quick start
> Register a panel, one Resource class, and visit /admin.
Source: https://streams.dev/docs/ui/quick-start
This walkthrough registers an admin panel with one resource backed by a Core stream.
## 1. Define a stream
`streams/posts.json`:
```json
{
"id": "posts",
"fields": [
{ "handle": "title", "type": "string", "required": true },
{ "handle": "body", "type": "string" }
]
}
```
## 2. Create a resource class
`app/Ui/Posts/PostResource.php`:
```php
namespace App\Ui\Posts;
use Streams\Ui\Resources\Resource;
use Streams\Ui\Builders\Forms\Form;
use Streams\Ui\Builders\Tables\Table;
use Streams\Ui\Builders\Forms\Layouts\Field;
use Streams\Ui\Builders\Inputs\TextInput;
use Streams\Ui\Builders\Inputs\TextareaInput;
use Streams\Ui\Builders\Tables\Columns\TextColumn;
use Streams\Ui\Livewire\Pages\ListEntries;
use Streams\Ui\Livewire\Pages\CreateEntry;
use Streams\Ui\Livewire\Pages\EditEntry;
class PostResource extends Resource
{
protected static ?string $stream = 'posts';
public static function getPages(): array
{
return [
'index' => ListEntries::route('/'),
'create' => CreateEntry::route('/create'),
'edit' => EditEntry::route('/{entry}/edit'),
];
}
public static function form(Form $form): Form
{
return $form->components([
Field::make('title')->input(TextInput::make('title')),
Field::make('body')->input(TextareaInput::make('body')),
]);
}
public static function table(Table $table): Table
{
return $table->columns([
TextColumn::make('title'),
]);
}
}
```
## 3. Register the panel
```php
UI::panel(
Panel::make('admin')
->default()
->path('admin')
->middleware(['web'])
->resources([PostResource::class])
);
```
## 4. Visit the panel
Open `/admin/posts` (resource slug derived from class name).
## Related
- [Resources](/docs/ui/resources)
- [Forms](/docs/ui/forms)
- [Tables](/docs/ui/tables)
---
# UI reference: Architecture
> Panels → pages/resources → Livewire → routes flow.
Source: https://streams.dev/docs/ui/architecture
Streams UI follows a Filament-inspired architecture: panels contain resources, resources define pages, and pages host Livewire components that resolve form and table builders.
## Request flow
```mermaid
flowchart LR
Request --> Middleware
Middleware --> SetUpPanel
SetUpPanel --> LivewirePage
LivewirePage --> Resource
Resource --> FormOrTable
FormOrTable --> CoreStream
```
1. HTTP request hits `/admin/{resource}/...`
2. `SetUpPanel` middleware boots the current panel via `UI::bootCurrentPanel()`
3. Livewire page (`ListEntries`, `CreateEntry`, `EditEntry`) mounts
4. Page delegates to `Resource::table()` or `Resource::form()`
5. Builders query or persist via Core `Streams::entries()`
## Key classes
| Layer | Namespace |
|-------|-----------|
| Panel | `Streams\Ui\Builders\Panels\Panel` |
| Resource | `Streams\Ui\Resources\Resource` |
| Form | `Streams\Ui\Builders\Forms\Form` |
| Table | `Streams\Ui\Builders\Tables\Table` |
| Pages | `Streams\Ui\Livewire\Pages\*` |
## Builders vs Livewire
Builders (`Form`, `Table`, `Action`) are configuration objects. Livewire pages (`InteractsWithForms`, `InteractsWithTable`) bind builder state to HTTP requests.
## Related
- [Panels](/docs/ui/panels)
- [Routing](/docs/ui/routing)
- [Livewire integration](/docs/ui/livewire)
---
# UI reference: Panels
> Path, middleware, default panel, branding, and SetUpPanel.
Source: https://streams.dev/docs/ui/panels
A **panel** is an admin area with its own path, middleware, navigation, resources, and pages.
## Register a panel
```php
use Streams\Ui\Support\Facades\UI;
use Streams\Ui\Builders\Panels\Panel;
UI::panel(
Panel::make('admin')
->default()
->path('admin')
->brandName('Acme')
->middleware(['web', 'auth'])
->resources([PostResource::class])
->pages([DashboardPage::class])
);
```
## Panel options
| Method | Purpose |
|--------|---------|
| `path()` | URL prefix (`/admin`) |
| `domain()` / `domains()` | Restrict to hostnames |
| `default()` | Mark as default panel for URL generation |
| `middleware()` | Additional middleware (always includes `panel:{id}`) |
| `brandName()`, logo traits | Branding |
| `homeUrl()` | Panel home link |
| `routes(Closure)` | Custom route registration |
## SetUpPanel middleware
Alias `panel` maps to `Streams\Ui\Http\Middleware\SetUpPanel`. It boots the current panel before Livewire handles the request.
## Related
- [Installation](/docs/ui/installation)
- [Theming](/docs/ui/theming)
- [Routing](/docs/ui/routing)
- [Navigation](/docs/ui/navigation)
---
# UI reference: Routing
> Panel route registration, route names, and custom routes closure.
Source: https://streams.dev/docs/ui/routing
Panel routes register automatically when you call `UI::panel()`. Resource pages define paths relative to the resource slug.
## Route naming
Pattern: `streams.ui.{panelId}.{resourceSlug}.{pageName}`
```php
PostResource::getUrl('edit', ['entry' => $post->id]);
// streams.ui.admin.posts.edit
```
## Resource page routes
```php
public static function getPages(): array
{
return [
'index' => ListEntries::route('/'),
'create' => CreateEntry::route('/create'),
'edit' => EditEntry::route('/{entry}/edit'),
];
}
```
## Panel path prefix
```php
Panel::make('admin')->path('admin');
// /admin/posts, /admin/posts/create, ...
```
## Custom routes
Pass a closure to `->routes()` on the panel:
```php
Panel::make('admin')
->path('admin')
->routes(function () {
Route::get('/settings', SettingsPage::class);
});
```
## Standalone pages
Register page classes on the panel:
```php
Panel::make('admin')->pages([DashboardPage::class]);
```
Standalone pages extend `Streams\Ui\Livewire\Pages\PanelPage` and define their own slug.
## Related
- [Pages](/docs/ui/pages)
- [Resources](/docs/ui/resources)
---
# UI reference: Resources
> PHP Resource subclasses — getPages(), table(), and form().
Source: https://streams.dev/docs/ui/resources
A **resource** connects a Core stream to admin CRUD pages. Subclass `Streams\Ui\Resources\Resource` and implement `getPages()`, `form()`, and `table()`.
## Basic resource
```php
class PostResource extends Resource
{
protected static ?string $stream = 'posts';
public static function getPages(): array
{
return [
'index' => ListEntries::route('/'),
'create' => CreateEntry::route('/create'),
'edit' => EditEntry::route('/{entry}/edit'),
];
}
public static function form(Form $form): Form
{
return $form->components([/* ... */]);
}
public static function table(Table $table): Table
{
return $table->columns([/* ... */]);
}
}
```
## Registration
```php
Panel::make('admin')->resources([PostResource::class]);
```
## Navigation
Override static properties or `getNavigationItems()`:
```php
protected static ?string $navigationGroup = 'Content';
protected static ?string $navigationLabel = 'Posts';
protected static ?string $navigationIcon = 'heroicon-o-document';
```
## URL helpers
```php
PostResource::getUrl('index');
PostResource::getUrl('edit', ['entry' => $id]);
```
## Does not exist
- `$panel->resource('posts', [...])` JSON registration
- Auto-generated forms from stream JSON alone (you implement `form()` and `table()`)
## Related
- [Pages](/docs/ui/pages)
- [Forms](/docs/ui/forms)
- [Tables](/docs/ui/tables)
---
# UI reference: Pages
> ListEntries, CreateEntry, EditEntry, and custom Livewire pages.
Source: https://streams.dev/docs/ui/pages
**Pages** are Livewire components rendered inside a panel layout.
## Resource CRUD pages
| Class | Purpose |
|-------|---------|
| `ListEntries` | Table listing |
| `CreateEntry` | Create form |
| `EditEntry` | Edit form |
Namespace: `Streams\Ui\Livewire\Pages`
Register through resource `getPages()`:
```php
return [
'index' => ListEntries::route('/'),
'create' => CreateEntry::route('/create'),
'edit' => EditEntry::route('/{entry}/edit'),
];
```
Each page sets `protected static string $resource = PostResource::class`.
## Custom pages
Extend `Streams\Ui\Livewire\Pages\PanelPage` for standalone screens:
```php
class DashboardPage extends PanelPage
{
protected static string $view = 'ui::pages.dashboard';
protected static ?string $navigationLabel = 'Dashboard';
}
```
Register on the panel:
```php
Panel::make('admin')->pages([DashboardPage::class]);
```
## Related
- [Resources](/docs/ui/resources)
- [Routing](/docs/ui/routing)
- [Livewire integration](/docs/ui/livewire)
---
# UI reference: Navigation
> Navigation groups, items, and wiring resources into the sidebar.
Source: https://streams.dev/docs/ui/navigation
Panel sidebar navigation comes from registered resources, pages, and explicit navigation configuration.
## Resource navigation
Set static properties on resource classes:
```php
protected static ?string $navigationGroup = 'Content';
protected static ?string $navigationLabel = 'Posts';
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static ?int $navigationSort = 10;
```
Or override `getNavigationItems()` for full control.
## Panel navigation groups
```php
Panel::make('admin')
->navigationGroups([
NavigationGroup::make('Content'),
NavigationGroup::make('Settings'),
]);
```
## Navigation items
```php
use Streams\Ui\Builders\Navigation\NavigationItem;
Panel::make('admin')->navigationItems([
NavigationItem::make('Dashboard')
->url('/admin')
->icon('heroicon-o-home'),
]);
```
## NavigationItem API
| Method | Purpose |
|--------|---------|
| `group()` | Assign to a group label |
| `icon()` | Icon name |
| `url()` | Link target |
| `sort()` | Sort order |
| `badge()` | Optional badge |
| `isActiveWhen()` | Active state closure |
## Related
- [Panels](/docs/ui/panels)
- [Resources](/docs/ui/resources)
---
# UI reference: Forms
> Form::make(), Form::for($livewire), components, state, and validation.
Source: https://streams.dev/docs/ui/forms
Forms are built with `Streams\Ui\Builders\Forms\Form` — a Livewire-aware view builder.
## Factory methods
```php
$form = Form::make('post'); // named form
$form = Form::for($livewire); // bound to Livewire component
Form::register($livewire); // register default form on page
$form = Form::resolve(); // resolve registered form
```
Resource pages call `Resource::form($form)` from `CreateEntry` and `EditEntry`.
## Schema API
Use `->components()` (not `schema()`):
```php
public static function form(Form $form): Form
{
return $form->components([
Field::make('title')
->input(TextInput::make('title')->required()),
Field::make('body')
->input(TextareaInput::make('body')),
]);
}
```
## State
Forms use `$statePath` (default derived from form name). Livewire pages expose public `$data` array synchronized with form state through `InteractsWithForms`.
## Validation
Form builder uses `HandlesValidation` concern. Validation runs on save actions defined in CreateEntry/EditEntry page logic.
## Does not exist
- `UI::form()` Blade helper
- `->schema()` method on Form
- Automatic form generation from stream JSON without PHP
## Related
- [Form layouts](/docs/ui/form-layouts)
- [Inputs](/docs/ui/inputs)
- [Livewire integration](/docs/ui/livewire)
---
# UI reference: Form layouts
> Field, Fieldset, Container, Section, and Grid layout components.
Source: https://streams.dev/docs/ui/form-layouts
Form layouts organize inputs in the admin UI. All layouts use `::make()` and `->components([...])`.
## Field
Wraps a single input:
```php
Field::make('title')
->label('Title')
->input(TextInput::make('title')),
```
Namespace: `Streams\Ui\Builders\Forms\Layouts\Field`
## Fieldset
Groups related fields:
```php
Fieldset::make('Meta')->components([
Field::make('slug')->input(TextInput::make('slug')),
]),
```
## Container
Generic wrapper for nested components:
```php
Container::make()->components([/* ... */]),
```
## Section and Grid
Use container builders from `Streams\Ui\Builders\Containers`:
```php
Section::make('Details')->components([
Grid::make()->columns(2)->components([
Field::make('first_name')->input(TextInput::make('first_name')),
Field::make('last_name')->input(TextInput::make('last_name')),
]),
]),
```
## Tabs
Tab navigation uses `Streams\Ui\Builders\Navigation\Tabs` for multi-section forms.
## Related
- [Forms](/docs/ui/forms)
- [Inputs](/docs/ui/inputs)
---
# UI reference: Inputs
> Input classes in Streams\\Ui\\Builders\\Inputs.
Source: https://streams.dev/docs/ui/inputs
Inputs define form controls. All extend `Streams\Ui\Builders\Inputs\Input` and use `InputClass::make('field_handle')`.
## Available inputs
| Class | Purpose |
|-------|---------|
| `TextInput` | Text, email, url, number (via type) |
| `TextareaInput` | Multi-line text |
| `SelectInput` | Single select |
| `CheckboxInput` | Checkbox |
| `RadioInput` | Radio group |
| `ToggleInput` | Boolean toggle |
| `DateInput` | Date picker |
| `DatetimeInput` | Datetime picker |
| `TimeInput` | Time picker |
| `FileInput` | File upload field |
| `ColorInput` | Color picker |
| `TagsInput` | Tag list |
| `MarkdownInput` | Markdown editor |
| `EditorInput` | Rich text editor |
## Example
```php
TextInput::make('title')
->label('Title')
->required()
->maxLength(255),
SelectInput::make('status')
->options(['draft' => 'Draft', 'published' => 'Published']),
```
## Usage in forms
Wrap inputs in `Field::make()` inside form `components()`:
```php
Field::make('email')->input(
TextInput::make('email')->type('email')
),
```
## Related
- [Forms](/docs/ui/forms)
- [Form layouts](/docs/ui/form-layouts)
---
# UI reference: Tables
> Table::make($livewire), query, columns, filters, and pagination.
Source: https://streams.dev/docs/ui/tables
Tables list stream entries in admin resources via `Streams\Ui\Builders\Tables\Table`.
## Basic table
```php
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')->sortable(),
TextColumn::make('created_at'),
])
->filters([
SearchFilter::make('search'),
])
->defaultSort('title');
}
```
## Factory
```php
Table::make($livewire); // used by ListEntries
Table::for($livewire); // alias
```
`ListEntries` passes the Livewire component automatically:
```php
public function table(Table $table): Table
{
return static::getResource()::table($table);
}
```
## Query
Table uses `HasQuery` and `HasStream` concerns to build a Core criteria instance from the resource's stream.
## Pagination, sorting, filters
Built-in concerns handle pagination, column sorting, and filter state. See [Columns](/docs/ui/columns), [Filters](/docs/ui/filters), and [Bulk actions](/docs/ui/bulk-actions).
## Does not exist
- `UI::table()` Blade helper
- Stream JSON `ui.tables` auto-wiring without PHP resource classes
## Related
- [Columns](/docs/ui/columns)
- [Filters](/docs/ui/filters)
- [Resources](/docs/ui/resources)
---
# UI reference: Columns
> Text, Link, Image, Icon, Toggle, and other table columns.
Source: https://streams.dev/docs/ui/columns
Columns display entry values in resource tables. All extend `Streams\Ui\Builders\Tables\Columns\Column`.
## Available columns
| Class | Purpose |
|-------|---------|
| `TextColumn` | Plain text |
| `LinkColumn` | Clickable link |
| `SelectColumn` | Select display |
| `ToggleColumn` | Inline toggle |
| `IconColumn` | Icon by value |
| `ImageColumn` | Thumbnail image |
| `ColorColumn` | Color swatch |
| `BadgeColumn` | Badge label |
| `ViewColumn` | Custom Blade view |
## Example
```php
TextColumn::make('title')
->label('Title')
->sortable()
->searchable(),
LinkColumn::make('title')
->url(fn ($entry) => PostResource::getUrl('edit', ['entry' => $entry])),
ToggleColumn::make('published'),
```
## Related
- [Tables](/docs/ui/tables)
- [Filters](/docs/ui/filters)
---
# UI reference: Filters
> Text, Select, Search, Toggle, and Criteria table filters.
Source: https://streams.dev/docs/ui/filters
Filters narrow table results. Register on the table builder via `->filters([...])`.
## Filter classes
| Class | Purpose |
|-------|---------|
| `SearchFilter` | Global search across searchable columns |
| `TextFilter` | Filter single field by text |
| `SelectFilter` | Filter by select options |
| `ToggleFilter` | Boolean filter |
| `CriteriaFilter` | Custom criteria closure |
Namespace: `Streams\Ui\Builders\Tables\Filters`
## Example
```php
->filters([
SearchFilter::make('search'),
SelectFilter::make('status')
->options(['draft' => 'Draft', 'published' => 'Published']),
ToggleFilter::make('featured'),
])
```
## Criteria filter
For advanced queries, use `CriteriaFilter` to modify the Core criteria instance directly.
## Related
- [Tables](/docs/ui/tables)
- [Core criteria](/docs/core/criteria)
---
# UI reference: Bulk actions
> BulkAction, selection patterns, and grouped actions.
Source: https://streams.dev/docs/ui/bulk-actions
Bulk actions operate on selected table rows. Register via `->bulkActions()` or `->groupedBulkActions()` on the table builder.
## BulkAction
```php
use Streams\Ui\Builders\Tables\BulkActions\BulkAction;
BulkAction::make('delete')
->label('Delete selected')
->action(function (Collection $records) {
$records->each->delete();
}),
```
Extends `Streams\Ui\Builders\Actions\Action`.
## Bulk action groups
```php
use Streams\Ui\Builders\Tables\BulkActions\BulkActionGroup;
->groupedBulkActions([
BulkActionGroup::make('Export')->actions([
BulkAction::make('csv')->action(/* ... */),
]),
])
```
## Built-in action
`DeleteSelectedEntries` is available for standard delete flows.
## Selection
Table Livewire state tracks selected entry keys. Bulk actions receive the selected record collection when invoked.
## Related
- [Tables](/docs/ui/tables)
- [Actions](/docs/ui/actions)
---
# UI reference: Actions
> Action, modals, redirects, and table header/row action groups.
Source: https://streams.dev/docs/ui/actions
Actions are buttons that run closures, open modals, or redirect. Base class: `Streams\Ui\Builders\Actions\Action`.
## Basic action
```php
use Streams\Ui\Builders\Actions\Action;
Action::make('publish')
->label('Publish')
->icon('heroicon-o-check')
->action(function ($entry) {
$entry->published = true;
$entry->save();
});
```
## Table actions
Register on the table builder:
```php
->actions([
Action::make('edit')->url(fn ($entry) => PostResource::getUrl('edit', ['entry' => $entry])),
])
->headerActions([
Action::make('create')->url(fn () => PostResource::getUrl('create')),
])
```
## Modals and forms
Actions support modal forms via `->form([...])` and redirect via `->redirect()` concerns on `MountableAction`.
## Action groups
`ActionGroup` and table-specific action classes organize related actions in dropdown menus.
## Related
- [Bulk actions](/docs/ui/bulk-actions)
- [Tables](/docs/ui/tables)
- [Livewire integration](/docs/ui/livewire)
---
# UI reference: Livewire integration
> InteractsWithTable, InteractsWithForms, and panel middleware.
Source: https://streams.dev/docs/ui/livewire
Streams UI admin pages are Livewire components. Traits connect page state to form and table builders.
## Page traits
| Trait | Used by | Namespace |
|-------|---------|-----------|
| `InteractsWithTable` | `ListEntries` | `Streams\Ui\Livewire\Tables` |
| `InteractsWithForms` | `CreateEntry`, `EditEntry` | `Streams\Ui\Livewire\Forms` |
| `InteractsWithActions` | `Page` base | `Streams\Ui\Builders\Actions\Contracts` |
## ListEntries
```php
class ListEntries extends PanelPage
{
use InteractsWithTable;
public function table(Table $table): Table
{
return static::getResource()::table($table);
}
}
```
## CreateEntry / EditEntry
```php
class CreateEntry extends PanelPage
{
use InteractsWithForms;
public function form(Form $form): Form
{
return static::getResource()::form($form);
}
}
```
## Panel middleware
`SetUpPanel` runs on every panel request and calls `UI::bootCurrentPanel()` so builders resolve the active panel, routes, and navigation.
Livewire persistent middleware includes `SetUpPanel` for subsequent requests.
## Related
- [Architecture](/docs/ui/architecture)
- [Pages](/docs/ui/pages)
- [Routing](/docs/ui/routing)
---
# UI reference: Theming
> Panel color palettes, brand name, logo, favicon, and layout.
Source: https://streams.dev/docs/ui/theming
Streams UI themes a panel by registering color palettes and sharing them as CSS variables. There is no site-wide theme switcher in Core. `streams/data/themes.json` in this docs site is leftover data; nothing in Core or UI reads it.
## Colors
`Panel::boot()` registers the panel's colors and shares `--{name}-{shade}` variables with the panel layout. The layout writes them on `:root`.
Defaults, from `ColorManager::getColors()`, before you set any:
| Name | Palette |
|------|---------|
| `danger` | `Color::Red` |
| `gray` | `Color::Zinc` |
| `info` | `Color::Blue` |
| `primary` | `Color::Amber` |
| `success` | `Color::Green` |
| `warning` | `Color::Amber` |
Each palette is shades `50` through `950`. The values are `r, g, b` strings (for example `245, 158, 11`), not full `rgb()` functions.
Override a name with a hex string or an `rgb()` string. `Color::hex()` and `Color::rgb()` generate the shade map from that color. Or pass the shade map yourself:
```php
use Streams\Ui\Builders\Panels\Panel;
use Streams\Ui\Colors\Color;
use Streams\Ui\Support\Facades\UI;
UI::panel(
Panel::make('admin')
->path('admin')
->colors([
'primary' => '#1d4ed8',
'gray' => Color::Slate,
])
);
```
`colors()` merges by name. Names you omit keep the defaults. A hex string has to start with `#`. An `rgb()` string has to start with `rgb`.
`Color::all()` lists the built-in palette names: slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose.
## Brand and favicon
```php
Panel::make('admin')
->brandName('Acme')
->brandLogo(asset('img/logo.svg'))
->favicon(asset('favicon.png'));
```
`getBrandName()` falls back to `config('app.name')`. The panel layout's head prints the favicon only when `getFavicon()` returns a value. Pass a root-absolute or fully qualified URL; a relative path breaks on nested admin URLs.
## Layout
`->layout('ui::layouts.app')` is the default. The view is a Blade view name, not a file path. Replace it when the panel should render inside your own shell. Color variables are shared with every view, but only a layout that prints `$cssVariables` (as `ui::layouts.partials.head` does) will emit them.
## Generated admins
Components generated by `php artisan streams:livewire` are not panels and don't use these settings. Their styling is the Tailwind classes in the generated Blade views, which you own. See the [command reference](/docs/sdk/commands#streamslivewire).
## Related
- [Panels](/docs/ui/panels)
- [Command reference](/docs/sdk/commands)
---
# API reference: Introduction
> A criteria-scoped REST API for streams and entries. You opt in to routes and own authentication.
Source: https://streams.dev/docs/api/introduction
Streams API (`streams/api`) turns your stream definitions into a REST API. It builds JSON responses for stream definitions and entries from the same [criteria](/docs/core/criteria) you use in PHP, so an endpoint returns exactly what `Streams::entries('posts')->where(...)->get()` would.
## What it is
- **Resources and endpoints.** `StreamsResource` (list, show, create, update, patch, delete) and `EntriesResource` (list, show, create, update, patch, delete, query).
- **Interfaces.** An `ApiInterface` groups resources under a path, domain, and middleware stack. You can register several (for example `v1` and `admin`).
- **A consistent envelope.** Every response has `data`, `meta`, `links`, and `errors` keys. See [Responses](/docs/api/responses).
- **Self-describing.** Responses carry a `self` link and `meta` (the query, payload, route parameters, and stream handle), and `GET /api/streams` returns the stream definitions themselves, which is what lets the [JavaScript client](/docs/client/introduction) discover the API.
- **Optional HTTP caching** through the `ApiCache` middleware (ETags and `Cache-Control`). See [Caching](/docs/api/caching).
- **Schema commands.** `php artisan api:schema` and `php artisan api:documentation` for OpenAPI output. See [OpenAPI](/docs/api/openapi).
## Why it exists
Streams already knows the shape of your data: fields, types, validation rules, and relationships live in `streams/*.json`. Writing CRUD controllers by hand repeats that knowledge and drifts from it. The API reads the stream definition at request time, so adding a field to a stream adds it to the API with no controller changes.
## What it does not do
- **It does not authenticate anyone.** The package has no users, tokens, or policies. Your application owns the middleware. See [Authentication](/docs/api/authentication).
- **It does not mount routes on its own.** Nothing is routed until you register an interface.
- **It is not JSON:API.** Request bodies are flat field maps. See [Requests](/docs/api/requests).
## How to use it
```bash
composer require streams/api:1.0.x-dev
```
```env
STREAMS_API_ENABLED=true
```
```php
// app/Providers/AppServiceProvider.php
use Streams\Api\Support\Facades\API;
public function boot(): void
{
API::routeCrud(); // streams + entries under /api
}
```
`GET /api/streams/posts/entries` now lists entries. Protect it before you deploy: see [Authentication](/docs/api/authentication).
## How to extend it
- Register more interfaces with their own paths and middleware: [Custom interfaces](/docs/api/custom-interfaces).
- Add endpoints and resources: [Custom endpoints](/docs/api/custom-endpoints).
- Resolve a tenant per request: [Tenancy](/docs/api/tenancy).
## Related
- [Installation](/docs/api/installation)
- [Routes](/docs/api/routes)
- [Responses](/docs/api/responses)
---
# API reference: Installation
> Require streams/api, enable the gate, and register an interface from a service provider.
Source: https://streams.dev/docs/api/installation
Install the API alongside [Core](/docs/core/installation).
## Require the package
```bash
composer require streams/api:1.0.x-dev
```
`streams/api` has no tagged release yet, so the version constraint is the `1.0` development branch. See [Versions and support](/docs/versions).
Laravel auto-discovers `Streams\Api\ApiServiceProvider`. It registers the `API` facade, the `api:schema` and `api:documentation` commands, and two middleware aliases: `api.gate` (`EnsureApiIsEnabled`) and `api.interface` (`SetUpApiInterface`).
## Publish configuration (optional)
```bash
php artisan vendor:publish --tag=config --provider="Streams\Api\ApiServiceProvider"
```
This writes `config/streams/api.php`. See [Configuration](/docs/api/configuration).
## Enable the gate
Every API route runs through the gate middleware (`gate_middleware`, `EnsureApiIsEnabled` by default). It returns `404 Not Found` until the API is enabled:
```env
STREAMS_API_ENABLED=true
```
## Register routes
No routes exist until you register an interface. Do it in a service provider's `boot()` method:
```php
// app/Providers/AppServiceProvider.php
use Streams\Api\Support\Facades\API;
public function boot(): void
{
API::routeCrud(); // streams + entries
// API::routeEntries(); // entries only
// API::routeStreams(); // stream definitions only
}
```
The helpers register the default interface (`STREAMS_API_DEFAULT_INTERFACE`, default `api`) at the configured prefix (`STREAMS_API_PREFIX`, default `api`) with the configured middleware group (`STREAMS_API_MIDDLEWARE`, default `api`). They do nothing if the default interface is already registered, so call only one of them: `routeEntries()` followed by `routeStreams()` registers the entry routes only.
> Don't call these helpers from `routes/api.php` or inside a `Route::prefix()->group()`. Registration happens immediately, so the surrounding group's prefix and middleware stack on top of the interface's own and you get paths like `/api/api/streams`.
For full control over the path, middleware, and resources, register an interface yourself:
```php
use Streams\Api\ApiInterface;
use Streams\Api\Resources\EntriesResource;
use Streams\Api\Support\Facades\API;
API::interface(
ApiInterface::make('api')
->path('api')
->middleware(['auth:sanctum'])
->resources([EntriesResource::class])
);
```
## Verify
```bash
php artisan route:list --name=streams.api
curl -s http://localhost/api/streams/posts/entries
```
## Related
- [Configuration](/docs/api/configuration)
- [Routes](/docs/api/routes)
- [Authentication](/docs/api/authentication)
---
# API reference: Configuration
> Every key in config/streams/api.php: enabled, prefix, middleware, interface, and the gate.
Source: https://streams.dev/docs/api/configuration
Configuration merges into `streams.api`. Publish `config/streams/api.php` to change it (see [Installation](/docs/api/installation)).
| Key | Env | Default | Purpose |
|-----|-----|---------|---------|
| `enabled` | `STREAMS_API_ENABLED` | `false` | Read by the default gate middleware. When `false`, API routes respond with `gate_status`. |
| `prefix` | `STREAMS_API_PREFIX` | `api` | Path used by `API::routeCrud()`, `routeEntries()`, and `routeStreams()`. |
| `default_interface` | `STREAMS_API_DEFAULT_INTERFACE` | `api` | ID of the default interface. Routes on other interfaces are named `streams.api.{id}.…`. |
| `middleware` | `STREAMS_API_MIDDLEWARE` | `api` | Middleware applied first to every interface's routes. Set an array in the config file for more than one. |
| `gate_middleware` | — | `EnsureApiIsEnabled::class` | Middleware applied second to every interface's routes. Point it at your own class to decide who gets in. |
| `gate_status` | `STREAMS_API_GATE_STATUS` | `404` | Status returned when the gate denies a request. |
| `gate_message` | `STREAMS_API_GATE_MESSAGE` | `Not Found` | Message returned when the gate denies a request. |
| `gate_except` | — | `[]` | Request paths (`$request->is()` patterns) that bypass the `enabled` check. |
## Middleware order
Every route on every interface gets this stack, in order:
1. `middleware` from config (Laravel's `api` group by default)
2. `gate_middleware` from config
3. `SetUpApiInterface`, which resolves the current interface from the route name and boots it
4. The interface's own `->middleware([...])`
5. Resource middleware (`ApiResource::$middleware`) and endpoint middleware
## Multiple middleware
The env var holds a single value. `STREAMS_API_MIDDLEWARE=api,auth:sanctum` does **not** work: Laravel reads it as one middleware named `api,auth` with a `sanctum` parameter. Use an array in the published config file instead:
```php
'middleware' => ['api', 'auth:sanctum', 'throttle:api'],
```
Or keep the config minimal and add middleware per interface (see [Authentication](/docs/api/authentication)).
## Related
- [Installation](/docs/api/installation)
- [Authentication](/docs/api/authentication)
- [Custom interfaces](/docs/api/custom-interfaces)
---
# API reference: Routes
> Full route table for streams and entries endpoints.
Source: https://streams.dev/docs/api/routes
Register routes with `API::routeCrud()` in a service provider's `boot()` method. It registers the default interface with both resources below. Paths below assume the default prefix `api`.
`API::routeStreams()` and `API::routeEntries()` register the same default interface with only one resource each. Use one helper, not several: each helper does nothing if the default interface is already registered, so calling `routeStreams()` and then `routeEntries()` leaves you with the stream routes only. To get both, call `routeCrud()`, or register your own [interface](/docs/api/custom-interfaces) with both resources.
## Stream routes (`StreamsResource`)
| Method | Path | Route name |
|--------|------|------------|
| GET | `/api/streams` | `streams.api.streams.list` |
| POST | `/api/streams` | `streams.api.streams.create` |
| GET | `/api/streams/{stream}` | `streams.api.streams.show` |
| PUT | `/api/streams/{stream}` | `streams.api.streams.update` |
| PATCH | `/api/streams/{stream}` | `streams.api.streams.patch` |
| DELETE | `/api/streams/{stream}` | `streams.api.streams.delete` |
Stream controllers operate on the meta-stream from `config('streams.core.streams_id')`.
## Entry routes (`EntriesResource`)
| Method | Path | Route name |
|--------|------|------------|
| GET | `/api/streams/{stream}/entries` | `streams.api.entries.list` |
| POST | `/api/streams/{stream}/entries` | `streams.api.entries.create` |
| GET | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.show` |
| PUT | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.update` |
| PATCH | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.patch` |
| DELETE | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.delete` |
| POST | `/api/streams/{stream}/query` | `streams.api.entries.query` |
The `{entry}` parameter accepts composite IDs (`where => ['entry' => '(.*)']`).
## Not valid
There is no shorthand `/api/{stream}` path. Always use `/api/streams/{stream}/entries`.
## Related
- [Stream endpoints](/docs/api/streams-endpoints)
- [Entry endpoints](/docs/api/entry-endpoints)
- [Query endpoint](/docs/api/query-endpoint)
---
# API reference: Request format
> Flat JSON field maps on create and update — not JSON:API.
Source: https://streams.dev/docs/api/requests
Create and update endpoints accept **flat JSON** (or form-encoded) field maps. The API does not use JSON:API resource objects or `data.attributes` envelopes for input.
## Create entry
`POST /api/streams/{stream}/entries`
```bash
curl -s -X POST "http://localhost/api/streams/films/entries" \
-H "Content-Type: application/json" \
-d '{"title":"Star Wars","director":"George Lucas"}'
```
The `CreateEntry` endpoint passes the JSON body to `newInstance()` on the stream's entries.
## Update entry
**PUT** sets the JSON body on the entry with `setAttributes()`; **PATCH** assigns each field in the body one at a time. Both return **200** when they update and **201** when the entry didn't exist and was created. See [Entry endpoints](/docs/api/entry-endpoints#update-entry).
```bash
curl -s -X PATCH "http://localhost/api/streams/films/entries/1" \
-H "Content-Type: application/json" \
-d '{"title":"Updated Title"}'
```
The route `{entry}` id is set on the payload from the URL segment.
## Create/update stream
Same flat JSON shape for stream definition entries on `/api/streams` endpoints.
## Query endpoint (different shape)
`POST /api/streams/{stream}/query` uses a `parameters` array — see [Query endpoint](/docs/api/query-endpoint).
## Related
- [Entry endpoints](/docs/api/entry-endpoints)
- [Responses](/docs/api/responses)
- [Errors](/docs/api/errors)
---
# API reference: Responses
> Response envelope — data, errors, links, meta — and ApiResponse API.
Source: https://streams.dev/docs/api/responses
Successful API responses use a consistent JSON envelope from `Streams\Api\ApiResponse`.
## Envelope shape
```json
{
"data": { "id": "1", "title": "Star Wars" },
"errors": [],
"links": {
"self": "http://localhost/api/streams/films/entries/1"
},
"meta": {
"stream": "films",
"query": {},
"parameters": { "stream": "films", "entry": "1" }
}
}
```
| Key | Purpose |
|-----|---------|
| `data` | Resource or collection (null when empty) |
| `errors` | Array of error strings |
| `links` | `self`, pagination links, create `location` |
| `meta` | `stream`, `query`, `payload`, `parameters`, pagination keys |
HTTP status is carried in the response header, not inside the JSON body.
## Status codes used
`200`, `201`, `204`, `400`, `404`, `409`
Validation failures return **409** with errors populated — not 422.
## Delete response
Successful delete returns **204 No Content** with an empty body (no envelope).
## Related
- [Errors](/docs/api/errors)
- [Pagination](/docs/api/pagination)
---
# API reference: Query parameters
> where, constraint, order_by, limit, skip, and per_page on GET entries.
Source: https://streams.dev/docs/api/query-parameters
`GET /api/streams/{stream}/entries` accepts query parameters implemented in `Streams\Api\Endpoints\Entries\ListEntries::applyFilters()` (the pre-1.0 `GetEntries` controller was replaced by this endpoint class).
## Parameters
| Parameter | Example | Purpose |
|-----------|---------|---------|
| `where[field]` | `where[director]=Lucas` | Equality filter |
| `constraint[field]` | `constraint[director]=like` | Operator: `like`, `>`, `>=`, `<`, `<=`, `!=`, `<>`, `in`, `between` |
| `order_by[field]` | `order_by[title]=ASC` | Sort direction |
| `limit` | `limit=10` | Max results (with `skip`) |
| `skip` | `skip=20` | Offset (default `0`) |
| `per_page` | `per_page=20` | Page size (default **100** in code) |
| `page` | `page=2` | Page number (default `1`) |
| `with` | `with=author,tags` or `with[]=author` | Eager-load relationship fields (a trailing `_id` is stripped from the handle) |
## Example
```bash
curl -s "http://localhost/api/streams/films/entries\
?where[director]=George%20Lucas\
&constraint[director]=like\
&order_by[title]=ASC\
&per_page=20&page=1"
```
Array values on `where` fields support operator/operand pairs for advanced filters.
## Related
- [Query endpoint](/docs/api/query-endpoint)
- [Pagination](/docs/api/pagination)
- [Core criteria](/docs/core/criteria)
---
# API reference: Pagination
> first_page, next_page, and meta keys from addPaginationMeta().
Source: https://streams.dev/docs/api/pagination
Paginated list responses add keys to `links` and `meta` via `ApiResponse::addPaginationMeta()`.
## Meta keys
```json
{
"meta": {
"total": 150,
"per_page": 100,
"last_page": 2,
"current_page": 1
}
}
```
## Link keys
```json
{
"links": {
"self": "...",
"first_page": "...",
"next_page": "...",
"previous_page": "..."
}
}
```
When on the first page, `previous_page` may be null. When on the last page, `next_page` may be null.
## Request parameters
Use `per_page` and `page` query parameters on `GET /api/streams/{stream}/entries`. Default `per_page` is **100** in source code.
## Related
- [Query parameters](/docs/api/query-parameters)
- [Responses](/docs/api/responses)
---
# API reference: Errors
> 409 validation, 404 JSON, 204 delete, and error envelope.
Source: https://streams.dev/docs/api/errors
Error responses populate the `errors` array in the JSON envelope (except 204 delete).
## Status codes
| Code | When | Body |
|------|------|------|
| **409** | Validation failure on create/update/patch | Envelope with `errors` strings |
| **404** | Stream or entry not found | Envelope with `errors` (e.g. `"Entry not found."`) |
| **204** | Successful delete | Empty body, no envelope |
| **400** | Bad request | Envelope with `errors` |
Validation uses **409 Conflict**, not HTTP 422.
## Example validation error
```json
{
"data": null,
"errors": ["The title field is required."],
"links": { "self": "..." },
"meta": { "stream": "films" }
}
```
## Example not found
`GET /api/streams/films/entries/missing` returns 404 with errors populated.
## Related
- [Responses](/docs/api/responses)
- [Request format](/docs/api/requests)
---
# API reference: Stream endpoints
> CRUD for stream definitions on /api/streams.
Source: https://streams.dev/docs/api/streams-endpoints
Stream endpoints manage entries in the meta-stream (`config('streams.core.streams_id')`) that store stream JSON definitions.
## List streams
```bash
curl -s "http://localhost/api/streams"
```
Returns collection in `data`.
## Show stream
```bash
curl -s "http://localhost/api/streams/posts"
```
## Create stream
```bash
curl -s -X POST "http://localhost/api/streams" \
-H "Content-Type: application/json" \
-d '{"id":"posts","name":"Posts","fields":[]}'
```
Returns **201** with `links.location`.
## Update stream
```bash
curl -s -X PUT "http://localhost/api/streams/posts" \
-H "Content-Type: application/json" \
-d '{"name":"Blog Posts"}'
```
PATCH accepts partial updates.
## Delete stream
```bash
curl -s -X DELETE "http://localhost/api/streams/posts"
```
Returns **204 No Content**.
## Related
- [Routes](/docs/api/routes)
- [Entry endpoints](/docs/api/entry-endpoints)
---
# API reference: Entry endpoints
> CRUD and upsert behavior for /api/streams/{stream}/entries.
Source: https://streams.dev/docs/api/entry-endpoints
Entry endpoints operate on any stream by handle.
## List entries
```bash
curl -s "http://localhost/api/streams/films/entries"
```
Supports [query parameters](/docs/api/query-parameters) and [pagination](/docs/api/pagination).
## Show entry
```bash
curl -s "http://localhost/api/streams/films/entries/1"
```
## Create entry
```bash
curl -s -X POST "http://localhost/api/streams/films/entries" \
-H "Content-Type: application/json" \
-d '{"title":"The Last Jedi","director":"Rian Johnson"}'
```
Returns **201** on success, **409** when validation fails.
## Update entry
PUT sets the payload on the entry with `setAttributes()`; PATCH assigns each field in the payload one at a time. Both validate and save, and return **200** with the entry, or **409** when validation fails. If the entry doesn't exist yet, both create it (the `{entry}` key from the URL becomes its key) and return **201**, the same as a create:
```bash
curl -s -X PATCH "http://localhost/api/streams/films/entries/4" \
-H "Content-Type: application/json" \
-d '{"title":"Patched Title"}'
```
## Delete entry
```bash
curl -s -X DELETE "http://localhost/api/streams/films/entries/4"
```
Returns **204** with empty body.
## Related
- [Request format](/docs/api/requests)
- [Query endpoint](/docs/api/query-endpoint)
---
# API reference: Query endpoint
> POST /api/streams/{stream}/query with parameters array.
Source: https://streams.dev/docs/api/query-endpoint
The query endpoint runs criteria operations from a JSON body instead of query string parameters.
## Request
`POST /api/streams/{stream}/query`
```bash
curl -s -X POST "http://localhost/api/streams/films/query" \
-H "Content-Type: application/json" \
-d '{
"parameters": [
{"where": ["director", "George Lucas"]},
{"orderBy": ["title", "asc"]},
{"limit": [20]}
]
}'
```
Each item in `parameters` maps to a criteria method call: the key is the method, the value is its argument list. Results are paginated like the list endpoint; pass `per_page` and `page` in the query string or body.
## Response
Same envelope as list endpoints: results in `data`, pagination in `meta` and `links`, and the request body echoed in `meta.payload`.
## Security
The endpoint calls **any** criteria method named in the body except `delete` and `truncate`. That includes write methods such as `create`, `save`, `firstOrCreate`, and `updateOrCreate`. Treat access to this endpoint as write access to the stream: put it behind the same middleware as your create and update endpoints, or leave `EntriesResource` off public interfaces and register only the endpoints you want (see [Custom endpoints](/docs/api/custom-endpoints)).
## When to use
Prefer GET with query parameters for simple filters. Use POST query when filter payloads are too large or complex for query strings.
## Related
- [Query parameters](/docs/api/query-parameters)
- [Core criteria](/docs/core/criteria)
---
# API reference: Custom interfaces
> Register several ApiInterface instances with their own path, domain, middleware, resources, and tenant.
Source: https://streams.dev/docs/api/custom-interfaces
An `ApiInterface` is one mounted API: a path (and optionally a domain), a middleware stack, and the resources and endpoints it serves. `API::routeCrud()` registers a single default interface for you. Register your own when you need versions, a separate admin API, or different auth for different consumers.
## Register an interface
```php
use Streams\Api\ApiInterface;
use Streams\Api\Resources\EntriesResource;
use Streams\Api\Resources\StreamsResource;
use Streams\Api\Support\Facades\API;
API::interface(
ApiInterface::make('v1')
->path('api/v1')
->middleware(['auth:sanctum', 'throttle:api'])
->resources([StreamsResource::class, EntriesResource::class])
);
```
Call `API::interface()` from a service provider's `boot()` method. Routes are registered immediately, so don't wrap the call in a route group.
## Builder methods
| Method | Purpose |
|--------|---------|
| `ApiInterface::make(?string $id)` | Create and configure an interface. The ID names routes and must be unique. |
| `path(string $path)` | URL prefix. If empty, the ID is used. |
| `domain(?string $domain)` / `domains(array $domains)` | Register the routes once per domain. |
| `middleware(array $middleware)` | Append middleware for every route on the interface. |
| `resources(array $classes)` | `ApiResource` classes to mount (for example `EntriesResource`). |
| `endpoints(array $endpoints)` | Extra endpoints: `EndpointRouter` instances or `'uri' => action` pairs (registered as GET). |
| `routes(?Closure $routes)` | A closure called with the interface once per domain. Its routes are registered **outside** the interface group, so they don't get the interface path or middleware; add those yourself. |
| `tenant(mixed $tenant)` | A tenant value or resolver closure for this interface. See [Tenancy](/docs/api/tenancy). |
All methods return the interface, and it also uses Laravel's `Conditionable` and `Tappable`, so `->when()` and `->tap()` work.
## Route names
Routes on the default interface (`STREAMS_API_DEFAULT_INTERFACE`, default `api`) are named `streams.api.{resource}.{endpoint}`, for example `streams.api.entries.list`. Routes on any other interface include its ID: `streams.api.v1.entries.list`.
`SetUpApiInterface` reads the route name on each request to decide which interface is current, so `API::currentApiInterface()` and `API::getTenant()` know which interface served the request.
## Configure every interface
`ApiInterface` supports `configureUsing()`, which runs a closure against each interface as it's made:
```php
use Streams\Api\ApiInterface;
ApiInterface::configureUsing(function (ApiInterface $interface) {
$interface->middleware(['throttle:api']);
});
```
## Subclass for reuse
Override `register()` or `boot()` to package an interface. `register()` runs when you pass it to `API::interface()`; `boot()` runs on the first request that hits one of its routes.
```php
use Streams\Api\ApiInterface;
use Streams\Api\Resources\EntriesResource;
class PartnerApi extends ApiInterface
{
protected function setUp(): void
{
$this->path('api/partners')
->middleware(['auth:sanctum'])
->resources([EntriesResource::class]);
}
}
API::interface(PartnerApi::make('partners'));
```
## Related
- [Authentication](/docs/api/authentication)
- [Custom endpoints](/docs/api/custom-endpoints)
- [Routes](/docs/api/routes)
---
# API reference: Custom endpoints
> Add endpoints to an interface, write ApiEndpoint classes, and group them in resources.
Source: https://streams.dev/docs/api/custom-endpoints
Custom endpoints live on an [interface](/docs/api/custom-interfaces), so they share its path, middleware, and gate.
## Quick endpoints
String keys passed to `endpoints()` are registered as GET routes inside the interface group:
```php
use Streams\Api\ApiInterface;
use Streams\Api\Support\Facades\API;
API::interface(
ApiInterface::make('api')
->path('api')
->endpoints([
'health' => fn () => response()->json(['status' => 'ok']),
'stats' => \App\Http\Controllers\StatsController::class,
])
);
// GET /api/health, GET /api/stats
```
## Endpoint classes
Extend `Streams\Api\Builders\Endpoints\ApiEndpoint`, implement `__invoke()`, and return `ApiResponse::make()`. The static `route()` method returns an `EndpointRouter` you can pass to `endpoints()` or a resource:
```php
namespace App\Api;
use Illuminate\Http\JsonResponse;
use Streams\Api\ApiResponse;
use Streams\Api\Builders\Endpoints\ApiEndpoint;
class FeaturedPosts extends ApiEndpoint
{
public function __invoke(): JsonResponse
{
$response = new ApiResponse('posts');
$criteria = $response->stream->entries();
$this->fire('apply', compact('criteria'));
return $response->make(
$criteria->where('featured', true)->limit(10)->get()
);
}
}
```
```php
->endpoints([
FeaturedPosts::route('posts/featured', 'get', 'posts.featured'),
])
```
`route(string $path, string|array $methods = 'get', ?string $routeName = null, array $where = [])` registers the route when the interface mounts. Endpoint instances also accept `routeMiddleware()` and `withoutRouteMiddleware()`.
Firing `apply` keeps your endpoint compatible with listeners such as [tenant scoping](/docs/api/tenancy).
## Resources
Group related endpoints in an `ApiResource`. Endpoint names are appended to the resource's route name prefix:
```php
namespace App\Api;
use Streams\Api\ApiResource;
class PostsResource extends ApiResource
{
protected static ?string $slug = 'posts';
protected static string|array $middleware = ['auth:sanctum'];
public static function getEndpoints(): array
{
return [
'featured' => FeaturedPosts::route('featured', 'get'),
];
}
}
```
With `usesRoutePrefix()` returning `true` (the default), routes are prefixed with the slug: `GET /api/posts/featured`, named `streams.api.posts.featured`. `EntriesResource` and `StreamsResource` return `false` because their paths already start with `streams/`.
## Override a built-in endpoint
Subclass a built-in endpoint and point a resource subclass at it:
```php
use Streams\Api\Endpoints\Entries\CreateEntry;
use Streams\Api\Resources\EntriesResource;
class AuditedCreateEntry extends CreateEntry
{
public function __invoke(string $stream): \Illuminate\Http\JsonResponse
{
$response = parent::__invoke($stream);
\Illuminate\Support\Facades\Log::info("API created an entry in {$stream}");
return $response;
}
}
class AppEntriesResource extends EntriesResource
{
public static function getEndpoints(): array
{
return [
...parent::getEndpoints(),
'create' => AuditedCreateEntry::route('streams/{stream}/entries', 'post'),
];
}
}
```
## Related
- [Custom interfaces](/docs/api/custom-interfaces)
- [Responses](/docs/api/responses)
- [Routes](/docs/api/routes)
---
# API reference: Authentication
> The API ships no auth. Your app owns access through the gate middleware and interface middleware.
Source: https://streams.dev/docs/api/authentication
Streams API does not authenticate or authorize anyone. It has no users, tokens, or policies. **Your application owns access control**, and the package gives you three places to put it.
This is deliberate: every app already has an auth system (Sanctum, Passport, session auth, signed URLs, an API gateway), and the API should use yours rather than add another.
> Registered API routes are public unless you add middleware. `STREAMS_API_ENABLED=true` opens them; it does not protect them.
## 1. The gate: `gate_middleware`
Every API route runs `config('streams.api.gate_middleware')`, which defaults to `Streams\Api\Http\Middleware\EnsureApiIsEnabled` (aliased as `api.gate`). The default gate only checks `streams.api.enabled` and `gate_except`, then aborts with `gate_status` (404) and `gate_message`.
Extend it the way you extend Laravel's `VerifyCsrfToken`:
```php
// app/Http/Middleware/EnsureApiIsEnabled.php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Streams\Api\Http\Middleware\EnsureApiIsEnabled as Middleware;
class EnsureApiIsEnabled extends Middleware
{
protected function shouldEnable(Request $request): bool
{
if (! parent::shouldEnable($request)) {
return false;
}
return $request->user()?->can('use-api') ?? false;
}
}
```
```php
// config/streams/api.php
'gate_middleware' => \App\Http\Middleware\EnsureApiIsEnabled::class,
```
Denied requests get `gate_status`, which is `404` by default so the API's existence isn't revealed. Set `STREAMS_API_GATE_STATUS=403` if you'd rather be explicit.
Because the gate runs after the `middleware` group, `$request->user()` is available when that group authenticates (for example `['api', 'auth:sanctum']`).
## 2. Interface middleware
Attach authentication to an interface. It applies to every resource and endpoint on that interface:
```php
use Streams\Api\ApiInterface;
use Streams\Api\Resources\EntriesResource;
use Streams\Api\Resources\StreamsResource;
use Streams\Api\Support\Facades\API;
// Public, read-mostly API
API::interface(
ApiInterface::make('api')
->path('api')
->middleware(['throttle:api'])
->resources([EntriesResource::class])
);
// Authenticated admin API, including stream definition CRUD
API::interface(
ApiInterface::make('admin')
->path('api/admin')
->middleware(['auth:sanctum', 'can:manage-streams'])
->resources([StreamsResource::class, EntriesResource::class])
);
```
Register interfaces in a service provider's `boot()` method, not inside a route group (see [Installation](/docs/api/installation)).
## 3. Resource and endpoint middleware
For finer control, subclass a resource and set its middleware:
```php
use Streams\Api\Resources\EntriesResource;
class ProtectedEntriesResource extends EntriesResource
{
protected static string|array $middleware = ['auth:sanctum'];
}
```
Endpoints built with the endpoint builder also accept `routeMiddleware()` and `withoutRouteMiddleware()`.
## Sanctum example
```php
// config/streams/api.php
'middleware' => ['api', 'auth:sanctum'],
```
```bash
curl -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
https://example.com/api/streams/posts/entries
```
With the [JavaScript client](/docs/client/introduction):
```javascript
import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://example.com/api',
middlewares: [new AuthorizationMiddleware({ token })],
});
```
## Checklist before you deploy
- Every interface you register has authentication, or you've decided it is public.
- `StreamsResource` (create, update, and delete stream definitions) is only on an admin-only interface, if at all.
- `POST /streams/{stream}/query` is protected like a write endpoint. It can call criteria write methods such as `create`. See [Query endpoint](/docs/api/query-endpoint).
- Rate limiting (`throttle:...`) is in the stack.
- The gate is your own class if access depends on more than a feature flag.
## Related
- [Configuration](/docs/api/configuration)
- [Custom interfaces](/docs/api/custom-interfaces)
- [Tenancy](/docs/api/tenancy)
---
# API reference: Tenancy
> Resolve a tenant once per request with API::tenant() or ApiInterface::tenant(), then scope criteria with endpoint callbacks.
Source: https://streams.dev/docs/api/tenancy
The API can resolve **one tenant per request** and hand it to your code. It does not decide what a tenant is or filter data for you: you supply a resolver, then use the tenant to scope queries.
For site-level multi-tenancy (different config, data paths, or streams per domain), see Core [Applications](/docs/core/applications) and the [Tenancy guide](/docs/tenancy).
## Resolve a tenant
Register a global resolver in a service provider:
```php
use Streams\Api\Support\Facades\API;
API::tenant(fn () => request()->user()?->organization_id);
```
Or set one per interface. An interface resolver takes precedence over the global one for routes on that interface:
```php
use Streams\Api\ApiInterface;
API::interface(
ApiInterface::make('partners')
->path('api/partners')
->middleware(['auth:sanctum'])
->tenant(fn () => request()->header('X-Partner'))
);
```
Read it anywhere during the request:
```php
$tenant = API::getTenant();
```
`API::getTenant()` calls the resolver once and binds the result in the container as `streams.api.tenant`, so later calls in the same request return the cached value. If no resolver is registered it returns `null`.
## Scope queries
`ListEntries` and `ShowEntry` fire an `apply` callback with the criteria before they run the query (`ListEntries` also fires `applied` after filters). Add a listener to scope every request:
```php
use Streams\Api\Endpoints\Entries\ListEntries;
use Streams\Api\Endpoints\Entries\ShowEntry;
use Streams\Api\Support\Facades\API;
use Streams\Core\Criteria\Criteria;
$scope = function (Criteria $criteria) {
if ($tenant = API::getTenant()) {
$criteria->where('organization_id', $tenant);
}
};
ListEntries::addCallbackListener('apply', $scope);
ShowEntry::addCallbackListener('apply', $scope);
```
The callback receives the criteria as `$criteria`, because listeners are called through the container with named parameters.
## What is not scoped
Only the list and show endpoints fire `apply`. Create, update, patch, delete, and query, plus all stream endpoints, do **not**. To enforce tenancy on writes:
- Put tenant checks in your [gate middleware](/docs/api/authentication) or interface middleware, or
- Replace the endpoints with your own subclasses (see [Custom endpoints](/docs/api/custom-endpoints)), or
- Keep tenants in separate data sources with Core [Applications](/docs/core/applications).
## Related
- [Authentication](/docs/api/authentication)
- [Custom interfaces](/docs/api/custom-interfaces)
- [Tenancy guide](/docs/tenancy)
---
# API reference: Caching
> HTTP caching with the ApiCache middleware: ETags, Cache-Control, and per-stream opt-out.
Source: https://streams.dev/docs/api/caching
`Streams\Api\Http\Middleware\ApiCache` adds HTTP caching to API responses: an `ETag`, public `Cache-Control` headers, and `304 Not Modified` for clients that already have the current version. It is **not applied by default**. Add it to the interfaces that should use it.
Separately, `ApiResponse` adds a `Last-Modified` header on cacheable (GET/HEAD) requests when the response data has a `lastModified()` method.
## Why
Stream definitions and most entry lists change far less often than they are read. ETags let clients and CDNs skip downloading unchanged data, and the optional server-side cache saves re-running criteria for clients that ask for it.
## Enable it
```php
use Streams\Api\ApiInterface;
use Streams\Api\Http\Middleware\ApiCache;
use Streams\Api\Resources\EntriesResource;
use Streams\Api\Support\Facades\API;
API::interface(
ApiInterface::make('api')
->path('api')
->middleware([ApiCache::class])
->resources([EntriesResource::class])
);
```
## How it behaves
For each request, the middleware:
1. **Skips** non-cacheable methods (anything but GET and HEAD), requests with `Cache-Control: no-cache` or `Pragma: no-cache`, routes with no `{stream}` parameter (such as `GET /api/streams`), and streams with `config.cache.enabled` set to `false`.
2. If the request sends `Cache-Control: max-age=N`, **caches the whole response server-side** for `N` seconds in the stream's cache store, keyed on URL, method, body, and input. The response is returned without ETag headers.
3. Otherwise, runs the request and sets:
- `ETag` to a quoted MD5 of the response body
- `Cache-Control: public, max-age=TTL, s-maxage=TTL`, where TTL is the stream's `config.cache.ttl` (default 3600 seconds)
4. If the request's `If-None-Match` matches the ETag, responds `304 Not Modified` with no body.
## Configure per stream
The middleware reads the same `cache` block as Core's [query cache](/docs/core/caching):
```json
{
"config": {
"cache": {
"enabled": true,
"ttl": 600,
"store": "redis"
}
}
}
```
| Key | Effect on `ApiCache` |
|-----|----------------------|
| `enabled` | `false` turns HTTP caching off for this stream. Any other value (including unset) leaves it on. |
| `ttl` | `max-age` and `s-maxage` in seconds. Default `3600`. |
| `store` | Laravel cache store for the `max-age` server-side cache. Default is your app's default store. |
## Things to know
- Responses are marked `public`. Don't put `ApiCache` on interfaces that return per-user data behind shared caches or CDNs unless you also vary the cache key (for example with a `Vary: Authorization` header in your own middleware).
- The ETag is computed after the response is built, so a 304 saves bandwidth, not server work.
- The [JavaScript client](/docs/client/introduction) exports an `ETagMiddleware`, but in 3.0.0 it is a placeholder and does not send `If-None-Match` yet.
## Related
- [Core caching](/docs/core/caching)
- [Caching guide](/docs/caching)
- [Custom interfaces](/docs/api/custom-interfaces)
---
# API reference: OpenAPI
> The generic OpenAPI reference, plus api:schema and api:documentation for your own app.
Source: https://streams.dev/docs/api/openapi
## Reference spec
The `streams/api` package ships an OpenAPI 3 description of its built-in endpoints in `resources/openapi/openapi.yaml`, and this site serves a copy at [/docs/api/openapi.yaml](/docs/api/openapi.yaml). It covers every route on `StreamsResource` and `EntriesResource` (the paths `API::routeCrud()` registers), the query parameters, and the response envelope. The package's own tests check that every registered route is documented and nothing else. Entry bodies are generic there because they depend on your streams.
In your own app the file is at `vendor/streams/api/resources/openapi/openapi.yaml`. The copy on this site is re-synced from the package with `php scripts/sync-openapi.php`; its header comment names the branch and commit it came from.
## Your app's spec
Generate an OpenAPI document with per-stream schemas from your application's stream definitions.
`api:schema` builds a tag, a component schema, and the `/streams/{id}/entries` and `/streams/{id}/entries/{entry}` paths for every stream. It does not include the stream-definition endpoints, the query endpoint, or custom endpoints, and its `info` block (contact, license) is placeholder text you should edit before publishing.
## Dump schema
```bash
php artisan api:schema
```
Writes OpenAPI YAML via `ApiSchema::create()`. Default output: `api.yaml` at project root.
Custom path:
```bash
php artisan api:schema storage/api/openapi.yaml
```
## Swagger UI
```bash
php artisan api:documentation
```
Copies Swagger UI to `public/swagger/` and runs `api:schema public/swagger/api.yaml`.
View at: `http://your-app.test/swagger/index.html`
## Related
- [Routes](/docs/api/routes)
- [Examples](/docs/api/examples)
---
# API reference: Testing
> Test your API routes with Laravel HTTP tests, and the status codes to expect.
Source: https://streams.dev/docs/api/testing
Test your app's API routes with ordinary Laravel HTTP tests. `streams/api` has no test base class for apps: its `Streams\Api\Tests\ApiTestCase` is in the package's `autoload-dev`, so it isn't autoloaded when you install the package, and it extends the `streams/testing` harness, which is Laravel 10 only for now.
## HTTP test
Routes exist once your app registers them, for example with `API::routeCrud()` in a service provider's `boot()` method (see [Installation](/docs/api/installation#register-routes)). Your normal `Tests\TestCase` boots the app, so the routes are there:
```php
namespace Tests\Feature;
use Illuminate\Support\Facades\URL;
use Tests\TestCase;
class FilmsApiTest extends TestCase
{
public function test_it_lists_films(): void
{
$this->getJson(URL::route('streams.api.entries.list', ['stream' => 'films']))
->assertOk()
->assertJsonStructure(['data', 'errors', 'links', 'meta']);
}
public function test_it_creates_a_film(): void
{
$this->postJson(URL::route('streams.api.entries.create', ['stream' => 'films']), [
'title' => 'The Last Jedi',
])->assertCreated();
}
}
```
If you register the routes conditionally (for example only when an env flag is set), set that flag in `phpunit.xml` or call `API::routeCrud()` in your test's `setUp()`.
Tests that create or delete entries write to the stream's source. Point the source at a temporary path in tests, or restore the data afterwards (see [Testing configuration](/docs/testing/configuration)).
## Status expectations
| Operation | Expected status |
|-----------|-----------------|
| List/show | 200 |
| Create | 201 |
| Update (PUT/PATCH) an existing entry | 200 |
| Update (PUT/PATCH) an entry that doesn't exist | 201 (it is created) |
| Validation failure | 409 |
| Not found (show/delete) | 404 |
| Delete success | 204 |
## Related
- [Errors](/docs/api/errors)
- [Hub: Testing](/docs/testing)
---
# API reference: Examples
> curl recipes with correct paths and response shapes.
Source: https://streams.dev/docs/api/examples
Complete curl examples assuming default prefix `api` and local server at `http://127.0.0.1:8000`.
## List entries
```bash
curl -s "http://127.0.0.1:8000/api/streams/films/entries"
```
## Filter and paginate
```bash
curl -s "http://127.0.0.1:8000/api/streams/films/entries\
?where[director]=Lucas&per_page=10&page=1"
```
## Create entry
```bash
curl -s -X POST "http://127.0.0.1:8000/api/streams/films/entries" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"title":"A New Hope","director":"George Lucas"}'
```
## Update entry
```bash
curl -s -X PATCH "http://127.0.0.1:8000/api/streams/films/entries/1" \
-H "Content-Type: application/json" \
-d '{"title":"Star Wars: A New Hope"}'
```
## Delete entry
```bash
curl -s -o /dev/null -w "%{http_code}" \
-X DELETE "http://127.0.0.1:8000/api/streams/films/entries/1"
# 204
```
## Query endpoint
```bash
curl -s -X POST "http://127.0.0.1:8000/api/streams/films/query" \
-H "Content-Type: application/json" \
-d '{"parameters":[{"where":["director","George Lucas"]}]}'
```
## Related
- [Entry endpoints](/docs/api/entry-endpoints)
- [Request format](/docs/api/requests)
- [Responses](/docs/api/responses)
---
# SDK reference: Introduction
> Dev-only Artisan generators and checks for streams, entries, addons, and Livewire components, plus a local MCP server for agents.
Source: https://streams.dev/docs/sdk/introduction
`streams/sdk` is a dev dependency. It registers Artisan generators and publishes example stream JSON. It is not part of the runtime your application serves.
Install it with Core already required:
```bash
composer require --dev streams/sdk:1.0.x-dev
```
Commands are registered only when the app is running in the console. The ones `php artisan` can see today are `make:stream`, `make:entry`, `make:addon`, `streams:list`, `streams:validate`, `streams:schema`, and `streams:livewire`. Arguments, options, and the commands that exist in source but are not registered are in the [command reference](/docs/sdk/commands).
There is no `streams:component` or `streams:crud` command.
## What it is for
Use the SDK to write the first version of a stream file, an entry, an addon package, or a JSON schema export, to check definitions with `streams:validate`, and to generate Livewire index, form, and show components with `streams:livewire`. Configured control panels belong to [Streams UI](/docs/ui/introduction), not to these generators. `streams:admin` has been removed.
The SDK also ships a local-development [MCP server](/docs/mcp) for AI agents. Start it with `php artisan mcp:start streams`. It needs `laravel/mcp`, which needs Laravel 11.45+ or 12.41+. On Laravel 10, agents use `streams:list --json` and `streams:validate --json` instead.
The SDK is on the `1.0.x-dev` branch and has no tagged release yet. See [Versions and support](/docs/versions).
## What a stream file looks like
`make:stream` writes a minimal file. A stream you actually use names its fields. Field behavior is Core's, documented in [Fields](/docs/core/fields):
```json
{
"$schema": "https://streams.dev/schema/streams.schema.json",
"name": "Blog Posts",
"fields": [
{"handle": "id", "type": "uuid"},
{"handle": "title", "type": "string", "required": true},
{"handle": "slug", "type": "slug", "unique": true},
{"handle": "author", "type": "relationship", "config": {"related": "users"}}
]
}
```
The SDK also publishes the example streams in its `streams/` directory (`blog_posts`, `products`, `contacts`, `files`, and `docs`) with `php artisan vendor:publish --tag=examples`.
## How to extend it
`make:entry` asks for missing fields through console inputs bound as `streams.console.inputs.{type}`. Replace that map in `config/streams/console.php` when you add a field type. Copy the defaults from `SdkServiceProvider::registerInputs()` first; a config value replaces the map rather than merging into it. Details are in the [command reference](/docs/sdk/commands).
## Related
- [Command reference](/docs/sdk/commands)
- [Core streams](/docs/core/streams)
- [Core fields](/docs/core/fields)
- [Stream definition schema](/docs/sdk/stream-schema)
- [MCP server](/docs/mcp)
- [Agents](/docs/agents)
---
# SDK reference: Command reference
> Every Artisan command in streams/sdk: what it writes, its arguments and options, JSON output for agents, and the command classes that are not registered.
Source: https://streams.dev/docs/sdk/commands
`streams/sdk` adds Artisan commands to generate and check streams, entries, addons, schemas, and Livewire components. Install it as a dev dependency:
```bash
composer require --dev streams/sdk:1.0.x-dev
```
Commands are registered only when the app runs in the console. Run `php artisan list` to see what your installed version provides.
## Available commands
| Command | What it does |
|---------|--------------|
| [`make:stream`](#makestream) | Writes a validated `streams/{id}.json` |
| [`make:entry`](#makeentry) | Creates or updates an entry in the stream's source |
| [`make:addon`](#makeaddon) | Scaffolds an addon package in `addons/{vendor}/{name}/` |
| [`streams:list`](#streamslist) | Lists registered streams, as a table or JSON |
| [`streams:validate`](#streamsvalidate) | Checks definitions against the JSON Schema and the running app |
| [`streams:schema`](#streamsschema) | Writes a `{id}.schema.json` entry schema per stream |
| [`streams:livewire`](#streamslivewire) | Generates Livewire 3 index, form, and show components |
Agents can call the same operations through the [MCP server](/docs/mcp) (`php artisan mcp:start streams`), which needs `laravel/mcp` and Laravel 11.45+ or 12.41+.
### make:stream
```bash
php artisan make:stream {id} {--name=} {--description=} {--force}
```
Writes `streams/{id}.json`. The ID must be snake_case (lowercase letters, numbers, and underscores, starting with a letter), and it is also the file name. `--name` defaults to the ID word-cased (`blog_posts` becomes "Blog Posts"). The file sets `config.source.format` to `json` without a `source.type`, so the app's default adapter is used, and it has one `id` field of type `uuid` with `config.default: true`.
The definition is validated (the same checks as [`streams:validate`](#streamsvalidate)) before it is written, then registered. The command refuses to replace an existing file unless you pass `--force`, and it refuses an ID that the app or an addon already registers.
```bash
php artisan make:stream blog_posts --description="Articles on the blog."
# Stream created: streams/blog_posts.json
```
The file starts with `"$schema": "https://streams.dev/schema/streams.schema.json"`. That URL is served by this site at [/schema/streams.schema.json](/schema/streams.schema.json), so editors that understand `$schema` validate and autocomplete the file.
### make:entry
```bash
php artisan make:entry {stream} {input?} {--update}
```
Creates an entry in `{stream}`. `input` is query-string formatted (`title=Hello&status=draft`). The command prompts for every field you didn't provide, validates the input against the stream's rules, then saves. With `--update`, validation treats the entry as existing, and if the input includes the stream's key (`key_name`, default `id`) and that entry exists, its attributes are updated.
```bash
php artisan make:entry posts "title=Hello&status=draft"
php artisan make:entry posts "id=hello&status=live" --update
```
Validation errors are printed and nothing is saved. On success the saved entry is printed as JSON.
### make:addon
```bash
php artisan make:addon {vendor/name} {--description=} {--force}
```
Scaffolds a Composer package under `addons/{vendor}/{name}/` with a `composer.json` and a service provider at `src/{Name}Provider.php`. The name must be a valid Composer package name. Without `--description` the command asks for one. It refuses to overwrite an existing addon unless you pass `--force`.
```bash
php artisan make:addon acme/reviews --description="Product reviews."
# Created: addons/acme/reviews/composer.json
# Created: addons/acme/reviews/src/ReviewsProvider.php
```
Add the directory as a Composer path repository to install it. See [Addons](/docs/addons).
### streams:list
```bash
php artisan streams:list {--json}
```
Lists every registered stream, sorted by ID, with its name, source type, field count, and description. `--json` prints an array instead, which is what scripts and agents should read:
```json
[
{
"id": "posts",
"name": "Posts",
"description": "Blog posts.",
"source": "filebase",
"fields": ["id", "title", "status", "author_id"],
"extends": null
}
]
```
### streams:validate
```bash
php artisan streams:validate {paths?*} {--json}
```
Validates stream definitions. With no paths it checks every `streams/*.json`. Each file is checked in three steps, and later steps run only when earlier ones pass:
1. The [stream definition JSON Schema](/schema/streams.schema.json) (the same file `$schema` points at).
2. The running app: field types must be registered, `extends` must name a registered stream, adapter and model classes must exist, and `related` must sit inside `config`. A related stream that is not registered yet is a warning. So is an `@` import that does not resolve.
3. A real build with `Streams::build()`.
```bash
php artisan streams:validate
php artisan streams:validate streams/posts.json streams/authors.json --json
```
The table output ends with `N checked, M invalid.`. `--json` prints an object keyed by file, each with `valid`, `errors`, and `warnings`. The exit code is non-zero when any file is invalid, so the command works as a CI or pre-commit check.
### streams:schema
```bash
php artisan streams:schema {--include=} {--exclude=} {--path=}
```
Writes a JSON schema (`{id}.schema.json`) for the *entries* of every registered stream, built from Core's `StreamSchema`: the stream's tag metadata merged with its object schema. This is not the definition schema that `$schema` points at. `--include` and `--exclude` take comma-separated stream IDs. `--path` is relative to the project root and must already exist; it defaults to the project root.
```bash
mkdir -p storage/schemas
php artisan streams:schema --include=posts,authors --path=storage/schemas
```
### streams:livewire
```bash
php artisan streams:livewire {stream} {--type=all} {--force}
```
Generates Livewire 3 components for a stream. `--type` is `index`, `form`, `show`, or `all` (the default). The components use the stream's repository and criteria, so they work with any source adapter.
| Type | Class | What it does |
|------|-------|--------------|
| `index` | `{Stream}Index` | Paginated, sortable table with a delete action |
| `form` | `{Stream}Form` | Create and edit form, validated with rules taken from the stream's fields |
| `show` | `{Stream}Show` | Read-only view of one entry |
Classes go in `config('livewire.class_namespace')` (default `App\Livewire`, so `app/Livewire/BlogPostsIndex.php`). Views go in `resources/views/livewire/`, named after the class in kebab case (`blog-posts-index.blade.php`). Protected fields are never rendered, and a generated `integer` or `uuid` key is left out of the form.
The command stops without writing anything if any target file exists, unless you pass `--force`. It does not register routes. It prints the routes to add to `routes/web.php`:
```php
Route::get('/blog-posts', \App\Livewire\BlogPostsIndex::class)->name('blog_posts.index');
Route::get('/blog-posts/create', \App\Livewire\BlogPostsForm::class)->name('blog_posts.create');
Route::get('/blog-posts/{entry}/edit', \App\Livewire\BlogPostsForm::class)->name('blog_posts.edit');
Route::get('/blog-posts/{entry}', \App\Livewire\BlogPostsShow::class)->name('blog_posts.show');
```
The components are full-page, so they render inside your Livewire layout (`config('livewire.layout')`). Use [Streams UI](/docs/ui/introduction) instead when you want a configured control panel rather than files you own and edit.
`streams:admin` has been removed. Generate the components with `streams:livewire` and put them behind your own layout and routes.
## Not registered
These command classes exist in the SDK source but are **not registered**, so `php artisan` won't find them:
| Command | Intended purpose | State |
|---------|------------------|-------|
| `streams:show` | Show one stream's attributes | Implemented, registration commented out. Use the MCP `describe-stream` tool. |
| `entries:list` | Paginated table of a stream's entries | Implemented, registration commented out. Use the MCP `list-entries` tool. |
| `entries:show` | Show one entry | Implemented, registration commented out. Use the MCP `read-entry` tool. |
| `streams:describe` | Write `streams/{id}.json` by inspecting a URL, JSON, database table, or Eloquent model | Implemented, registration commented out |
| `streams:tap` | Call a "tap" URL with query-string input | Not implemented (empty handler) |
## How to extend it
`make:entry` prompts for each missing field through a console input bound as `streams.console.inputs.{type}`. `SdkServiceProvider::registerInputs()` binds string, boolean, select/enum, array, and object inputs. `number`, `decimal`, `date`, `time`, and `datetime` use the string input. `integer` is listed twice in that map; the later entry wins, so `integer` is also prompted as a string and `IntegerConsoleInput` is never bound. To prompt for a custom field type, set the full map in `config('streams.console.inputs')`. The config value replaces the default map, so copy the defaults from `registerInputs()` and add yours:
```php
// config/streams/console.php
return [
'inputs' => [
// ...the SDK defaults...
'money' => \App\Console\Inputs\MoneyConsoleInput::class,
],
];
```
## Related
- [SDK introduction](/docs/sdk/introduction)
- [MCP server](/docs/mcp)
- [Stream definition schema](/docs/sdk/stream-schema)
- [Core streams](/docs/core/streams)
---
# SDK reference: Stream definition schema
> The JSON Schema for streams/*.json, served at /schema/streams.schema.json: what it checks, editor setup, and validating with streams:validate.
Source: https://streams.dev/docs/sdk/stream-schema
Stream definitions are the JSON files in `streams/` (for example `streams/posts.json`). `streams/sdk` ships a [JSON Schema](https://json-schema.org) (draft-07) for them in `vendor/streams/sdk/resources/schemas/streams.schema.json`, and this site serves the same file at:
```text
https://streams.dev/schema/streams.schema.json
```
That URL is the schema's `$id`. It is also the `$schema` that `make:stream` and the `make-stream` MCP tool write into new definitions, so editors that follow `$schema` validate and autocomplete stream files with no setup.
## What it checks
- Top-level keys: `id`, `name`, `description`, `extends`, `config`, `fields`, `rules`, `route`, `routes`, `data`, `ui`.
- `config.source`: known source types (`filebase`, `file`, `self`, `database`, `eloquent`, `filesystem`, `collection`, `elasticsearch`, `opensearch`, or an adapter class) and formats (`json`, `yaml`, `md`, `html`, `tpl`, `csv`).
- Both field forms: a list of field objects with `handle`, or an object keyed by handle whose values are a field object or a type string.
- Imports: the whole `fields` value, or any single field, can be `"@path/to/file.json"`. Core replaces it with the decoded file, relative to the app root, when it builds the stream.
- Field types: the core types are offered for completion. Other lowercase names are allowed because apps and addons can register their own types.
- Type-specific config: `relationship` fields need `config.related` (one stream ID, or a list of IDs for polymorphic and multi-target fields). `select`, `enum`, and `multiselect` fields need `config.options`. `object` fields list `config.allowed` as `{"stream": ...}`, `{"generic": ...}`, or `{"prototype": ...}` objects. `eloquent` sources need `config.source.model`.
- `data`: inline entries for a `self` source.
- `input` on a field: a form hint for Streams UI. Core does not read it.
Unknown top-level keys are allowed because addons extend definitions.
Two shapes show up in older examples and are ignored by Core. `streams:validate` reports both:
- `"related"` next to `"type"`. The value has to be `config.related`.
- `"default"` next to `"type"`. The value has to be `config.default` (for a `uuid` field, `"default": true` generates a UUID).
## Validate from the command line
```bash
php artisan streams:validate # every streams/*.json
php artisan streams:validate streams/posts.json # specific files
php artisan streams:validate --json # machine-readable output
```
The command exits non-zero if any file is invalid. Besides the schema, it checks the running app:
- field types are registered,
- `extends` streams exist,
- `config.related` streams exist (a warning, since you may be creating them together),
- `@` imports resolve (a warning),
- model and adapter classes exist, and
- Core can build the definition.
Agents connected to the [MCP server](/docs/mcp) get the same checks from `validate-stream-definition`, and the schema from `definition-schema` or the `streams://schemas/streams.schema.json` resource. See the [command reference](/docs/sdk/commands#streamsvalidate).
## Use it in your editor
Add `$schema` to a definition:
```json
{
"$schema": "https://streams.dev/schema/streams.schema.json",
"name": "Posts",
"fields": []
}
```
To validate offline, or against the exact version you have installed, map stream files to the local copy. In VS Code, add this to `.vscode/settings.json`:
```json
{
"json.schemas": [
{
"fileMatch": ["**/streams/*.json"],
"url": "./vendor/streams/sdk/resources/schemas/streams.schema.json"
}
]
}
```
## Related
- [Command reference](/docs/sdk/commands)
- [MCP server](/docs/mcp)
- [Core streams](/docs/core/streams)
- [Core fields](/docs/core/fields)
---
# SDK reference: Streams
> Define streams with the SDK scaffolding workflow.
Source: https://streams.dev/docs/sdk/streams
Streams are the foundation of Laravel Streams - they define the structure and behavior of your data entities. Think of them as dynamic Eloquent models that can be configured through JSON.
## Basic Stream Structure
```json
{
"$schema": "https://streams.dev/schema/streams.schema.json",
"name": "Blog Posts",
"handle": "blog_posts",
"description": "Content management for blog articles",
"config": {
"source": {
"type": "eloquent",
"model": "App\\Models\\BlogPost"
}
},
"fields": [
{
"handle": "id",
"type": "integer",
"config": {
"auto_increment": true
}
},
{
"handle": "title",
"type": "string",
"name": "Title",
"required": true,
"config": {
"max": 255
}
}
],
"routes": [
{
"handle": "index",
"uri": "blog",
"view": "blog.index"
}
]
}
```
## Stream Properties
### Required Properties
- **name**: Human-readable name for the stream
- **handle**: Unique identifier (snake_case recommended)
- **fields**: Array of field definitions
### Optional Properties
- **description**: Brief description of the stream's purpose
- **config**: Configuration options for data source and behavior
- **routes**: URL routing definitions
- **validation**: Stream-level validation rules
## Data Sources
### Eloquent Models
```json
{
"config": {
"source": {
"type": "eloquent",
"model": "App\\Models\\Product"
}
}
}
```
### File Storage (JSON/YAML)
```json
{
"config": {
"source": {
"format": "json",
"path": "content/products"
}
}
}
```
### Database Tables
```json
{
"config": {
"source": {
"type": "database",
"table": "products"
}
}
}
```
## Stream Configuration Options
### Pagination
```json
{
"config": {
"pagination": {
"per_page": 20,
"page_name": "page"
}
}
}
```
### Sorting
```json
{
"config": {
"sort": {
"field": "created_at",
"direction": "desc"
}
}
}
```
### Caching
```json
{
"config": {
"cache": {
"ttl": 3600,
"tags": ["products", "catalog"]
}
}
}
```
## Routing Integration
Streams can automatically generate routes for common operations:
```json
{
"routes": [
{
"handle": "index",
"uri": "products",
"view": "products.index"
},
{
"handle": "show",
"uri": "products/{id}",
"view": "products.show"
},
{
"handle": "api",
"uri": "api/products",
"uses": "App\\Http\\Controllers\\Api\\ProductController"
}
]
}
```
## AI Assistant Examples
When creating streams, consider these common patterns:
### Content Management
- Blog posts, pages, articles
- Categories, tags, taxonomies
- Media galleries, file management
### E-commerce
- Products, variants, inventories
- Orders, customers, payments
- Reviews, ratings, wishlists
### User Management
- Users, roles, permissions
- Profiles, preferences, settings
- Activity logs, notifications
### Business Applications
- Contacts, companies, leads
- Projects, tasks, timelines
- Invoices, payments, reports
## Best Practices
1. **Use Descriptive Handles**: Choose clear, descriptive identifiers
2. **Plan Relationships**: Consider how streams connect to each other
3. **Think Mobile-First**: Design for responsive interfaces
4. **Consider Performance**: Add appropriate indexes and caching
5. **Validate Early**: Define validation rules in the stream definition
6. **Document Purpose**: Add clear descriptions for future developers
---
# SDK reference: TALL Components
> Tailwind, Alpine, Laravel, and Livewire patterns in the SDK.
Source: https://streams.dev/docs/sdk/tall-components
This guide covers how the Laravel Streams SDK generates and works with TALL stack components (Tailwind CSS, Alpine.js, Laravel, and Livewire).
## Overview
The SDK automatically generates:
- **Tailwind CSS**: Utility-first styling with pre-built component classes
- **Alpine.js**: Lightweight JavaScript framework for interactivity
- **Laravel**: Backend controllers, routes, and middleware
- **Livewire**: Full-stack reactive components
## Generated Components
### Livewire Components
#### Index Component (List View)
```php
['except' => ''],
'sortField' => ['except' => 'created_at'],
'sortDirection' => ['except' => 'desc'],
];
public function updatingSearch()
{
$this->resetPage();
}
public function sortBy($field)
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortDirection = 'asc';
}
$this->sortField = $field;
}
public function render()
{
$stream = Streams::make('blog_posts');
$entries = $stream->entries()
->when($this->search, function ($query) {
$query->where('title', 'like', '%' . $this->search . '%')
->orWhere('content', 'like', '%' . $this->search . '%');
})
->orderBy($this->sortField, $this->sortDirection)
->paginate($this->perPage);
return view('livewire.blog-post-index', [
'entries' => $entries,
'stream' => $stream,
]);
}
}
```
#### Form Component (Create/Edit)
```php
'required|min:3|max:255',
'slug' => 'required|unique:blog_posts,slug',
'content' => 'required|min:10',
'featured_image' => 'nullable|image|max:2048',
'status' => 'required|in:draft,published,archived',
'published_at' => 'nullable|date',
];
public function mount($entryId = null)
{
if ($entryId) {
$this->entryId = $entryId;
$this->loadEntry();
}
}
public function loadEntry()
{
$stream = Streams::make('blog_posts');
$entry = $stream->repository()->find($this->entryId);
if ($entry) {
$this->fill($entry->toArray());
}
}
public function updatedTitle()
{
if (!$this->entryId) {
$this->slug = str()->slug($this->title);
}
}
public function save()
{
$this->validate();
$stream = Streams::make('blog_posts');
$data = [
'title' => $this->title,
'slug' => $this->slug,
'content' => $this->content,
'status' => $this->status,
'published_at' => $this->published_at,
];
if ($this->featured_image) {
$data['featured_image'] = $this->featured_image->store('blog-images', 'public');
}
if ($this->entryId) {
$entry = $stream->repository()->find($this->entryId);
$entry->update($data);
} else {
$entry = $stream->repository()->create($data);
}
session()->flash('message', 'Blog post saved successfully!');
return redirect()->route('blog-posts.index');
}
public function render()
{
return view('livewire.blog-post-form');
}
}
```
### Blade Templates
#### Index View
```blade
{{-- resources/views/livewire/blog-post-index.blade.php --}}
```
## Tailwind CSS Utilities
### Component Classes
```css
/* Custom component classes for streams */
.stream-card {
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow;
}
.stream-table {
@apply min-w-full divide-y divide-gray-200;
}
.stream-table th {
@apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider bg-gray-50;
}
.stream-table td {
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900;
}
.stream-form {
@apply space-y-6;
}
.stream-form .form-group {
@apply space-y-1;
}
.stream-form label {
@apply block text-sm font-medium text-gray-700;
}
.stream-form input[type="text"],
.stream-form input[type="email"],
.stream-form input[type="password"],
.stream-form textarea,
.stream-form select {
@apply mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm;
}
.btn-primary {
@apply inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500;
}
.btn-secondary {
@apply inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500;
}
.btn-danger {
@apply inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500;
}
```
## Usage with SDK Commands
`streams:livewire` is the only SDK command that generates components. It writes Livewire 3 classes and Blade views you then own and restyle with the patterns above:
```bash
# Index, form, and show components for a stream
php artisan streams:livewire blog_posts
# Or one component at a time
php artisan streams:livewire blog_posts --type=index
php artisan streams:livewire blog_posts --type=form
php artisan streams:livewire blog_posts --type=show
```
The command prints the routes to add to `routes/web.php`. There is no `streams:tall`, `streams:blade`, or `streams:admin` command. See the [command reference](/docs/sdk/commands#streamslivewire).
---
# SDK reference: AI Prompts
> Patterns and prompts for AI-assisted Streams development.
Source: https://streams.dev/docs/sdk/ai-prompts
This guide provides AI assistants with specific prompts, patterns, and examples for effectively using the Laravel Streams SDK to help developers build applications.
## AI Assistant Workflow
When helping users with Laravel Streams, follow this structured approach:
### 1. Requirements Gathering
Ask clarifying questions to understand the project:
```text
Before we start building with Laravel Streams, I need to understand your requirements:
1. What type of application are you building? (blog, e-commerce, CRM, etc.)
2. What data entities do you need to manage?
3. Do you need an admin panel for content management?
4. What are the key relationships between your data?
5. Do you have any specific UI/UX requirements?
6. Will this be used by multiple user types/roles?
```
### 2. Data Structure Analysis
Help users design their stream structure:
```text
Based on your requirements, I recommend these streams:
For a blog application:
- blog_posts: Main content with title, content, author, category
- categories: Organize posts by topic
- tags: Flexible labeling system
- users: Authors and administrators
Let me create the stream definitions for you.
```
### 3. Implementation Steps
Provide a clear roadmap:
```text
Here's how we'll implement your application:
1. Create stream definitions with proper field types
2. Generate Livewire components for CRUD operations
3. Build an admin panel for content management
4. Customize the UI to match your design requirements
5. Add any custom business logic
Let's start with step 1...
```
## Common Prompts for Stream Creation
### Blog System
```bash
# Create the main blog stream
php artisan make:stream blog_posts
# Generate the index, form, and show Livewire components
php artisan streams:livewire blog_posts
# Publish blog example for reference
php artisan vendor:publish --tag=blog-example
```
### E-commerce System
```bash
# Create product catalog
php artisan make:stream products
# Generate the index and form components
php artisan streams:livewire products --type=index
php artisan streams:livewire products --type=form
# Publish e-commerce example
php artisan vendor:publish --tag=ecommerce-example
```
### CRM System
```bash
# Create contact management
php artisan make:stream contacts
# Generate CRM components (index, form, and show)
php artisan streams:livewire contacts
# Publish CRM example
php artisan vendor:publish --tag=crm-example
```
## Field Type Recommendations
### Content Management
When users need content management, recommend these field types. Field behavior is Core's ([Fields](/docs/core/fields)): length and range limits go in `rules`, long text is a `string` with an `input` hint, and type options go in `config`:
```json
{
"fields": {
"title": {
"type": "string",
"required": true,
"rules": [
"max:255"
]
},
"slug": {
"type": "slug",
"unique": true
},
"content": {
"type": "string",
"required": true,
"input": {
"type": "editor"
}
},
"featured_image": {
"type": "image"
},
"status": {
"type": "select",
"config": {
"options": {
"draft": "Draft",
"published": "Published",
"archived": "Archived"
}
}
}
}
}
```
### E-commerce Fields
For product catalogs, suggest:
```json
{
"fields": {
"name": {
"type": "string",
"required": true
},
"sku": {
"type": "string",
"unique": true,
"required": true
},
"price": {
"type": "decimal",
"required": true,
"rules": [
"min:0"
],
"config": {
"precision": 2
}
},
"stock_quantity": {
"type": "integer",
"rules": [
"min:0"
],
"config": {
"default": 0
}
},
"is_featured": {
"type": "boolean",
"config": {
"default": false
}
}
}
}
```
### User Management
For user-related streams:
```json
{
"fields": {
"first_name": {
"type": "string",
"required": true,
"rules": [
"max:100"
]
},
"last_name": {
"type": "string",
"required": true,
"rules": [
"max:100"
]
},
"email": {
"type": "email",
"unique": true,
"required": true
},
"avatar": {
"type": "image"
},
"role": {
"type": "select",
"config": {
"options": {
"user": "User",
"admin": "Administrator",
"moderator": "Moderator"
}
}
}
}
}
```
## Customization Prompts
### UI Customization
When users want to customize the interface:
```text
I can help you customize the generated components. Here are common customizations:
1. **Styling**: Modify Tailwind CSS classes in the Blade templates
2. **Layout**: Change the grid layout or add/remove sections
3. **Functionality**: Add custom Alpine.js interactions
4. **Validation**: Enhance form validation rules
5. **Navigation**: Customize the admin panel navigation
Which aspect would you like to customize first?
```
### Business Logic
For adding custom functionality:
```text
To add custom business logic to your streams:
1. **Custom Methods**: Add methods to your Livewire components
2. **Event Listeners**: Set up event handling for real-time updates
3. **Validation Rules**: Create custom validation logic
4. **Relationships**: Define complex data relationships
5. **APIs**: Add API endpoints for external integrations
Let me show you how to implement [specific feature]...
```
## Troubleshooting Prompts
### Common Issues
Help users resolve typical problems:
```text
Let me help you troubleshoot this issue. Here are the most common problems and solutions:
1. **Missing Stream**: Make sure the stream definition exists in streams/ directory
2. **Component Errors**: Check that all required fields are defined
3. **Route Conflicts**: Verify routes are properly namespaced
4. **Permission Issues**: Ensure proper file permissions for generated files
5. **Asset Issues**: Run `npm run dev` to compile Tailwind CSS
Can you share the specific error message you're seeing?
```
### Performance Optimization
When users need performance improvements:
```text
Here are performance optimization strategies for your streams:
1. **Eager Loading**: Load related data efficiently
2. **Caching**: Cache expensive queries and computations
3. **Pagination**: Implement proper pagination for large datasets
4. **Indexing**: Add database indexes for frequently queried fields
5. **Image Optimization**: Compress and resize images automatically
Let me show you how to implement these optimizations...
```
## Advanced Patterns
### Multi-tenant Applications
For SaaS applications:
```php
// Add tenant filtering to streams
public function render()
{
$entries = $this->stream->entries()
->where('tenant_id', auth()->user()->tenant_id)
->paginate($this->perPage);
return view('livewire.blog-posts-index', compact('entries'));
}
```
### API Integration
For headless/API-first applications:
```php
// Add API endpoints
Route::apiResource('blog-posts', BlogPostApiController::class);
// In controller
public function index()
{
$stream = Streams::make('blog_posts');
return BlogPostResource::collection(
$stream->entries()->paginate(request('per_page', 15))
);
}
```
### Real-time Updates
For live data updates:
```php
// Add broadcasting to Livewire components
protected $listeners = ['postUpdated' => 'refreshPosts'];
public function refreshPosts()
{
$this->emit('$refresh');
}
// Broadcast events when data changes
public function save()
{
// Save logic here
broadcast(new PostUpdated($this->entry));
}
```
## Code Generation Examples
### Complete Blog Setup
```bash
# 1. Publish blog example
php artisan vendor:publish --tag=blog-example
# 2. Check the definition, then generate index, form, and show components
php artisan streams:validate streams/blog_posts.json
php artisan streams:livewire blog_posts
# 3. Add the printed routes to routes/web.php
```
### E-commerce Product Catalog
```bash
# 1. Publish product example
php artisan vendor:publish --tag=ecommerce-example
# 2. Generate product management
php artisan streams:livewire products --type=index
php artisan streams:livewire products --type=form
# 3. Add the printed routes to routes/web.php, then customize the
# generated classes in app/Livewire and views in resources/views/livewire
```
## Best Practices for AI Assistants
### 1. Always Ask Context Questions
Before generating code, understand:
- User's experience level
- Project requirements
- Existing codebase
- Performance needs
- Security requirements
### 2. Provide Complete Solutions
Don't just generate partial code:
- Include all necessary files
- Add proper validation
- Include error handling
- Provide styling
- Add documentation
### 3. Explain Choices
When recommending field types or patterns:
- Explain why you chose specific options
- Mention alternatives
- Discuss trade-offs
- Provide examples
### 4. Follow Security Best Practices
Always include:
- Proper validation rules
- CSRF protection
- Authorization checks
- Input sanitization
- SQL injection prevention
### 5. Consider Performance
Optimize generated code:
- Use pagination for large datasets
- Implement caching where appropriate
- Add database indexes
- Optimize images and assets
- Use eager loading for relationships
## Sample Conversations
### Blog Setup Conversation
```text
User: I want to create a blog with Laravel Streams.
AI: I'd be happy to help you create a blog with Laravel Streams! Let me ask a few questions to ensure we build exactly what you need:
1. Will you have multiple authors or just yourself?
2. Do you need categories and tags for organizing posts?
3. Do you want an admin panel for content management?
4. Any specific design requirements or should we use the default TALL stack styling?
Based on your answers, I'll generate the complete blog system for you.
```
### E-commerce Conversation
```text
User: I need to build a product catalog.
AI: Great! A product catalog is perfect for Laravel Streams. To build the right solution, I need to understand your requirements:
1. What types of products will you sell? (physical, digital, or both)
2. Do you need inventory management?
3. Will you have product variants (size, color, etc.)?
4. Do you need categories and brands?
5. Any specific pricing features (sales, bulk pricing, etc.)?
Once I understand your needs, I'll create the complete product management system with admin panel.
```
This guide ensures AI assistants can effectively help users build robust applications with the Laravel Streams SDK while following best practices and providing comprehensive solutions.
---
# Testing reference: Introduction
> Testing package for Streams with pre-configured environments and sample data.
Source: https://streams.dev/docs/testing/introduction
The Streams Testing package provides everything you need to test your Streams applications with confidence. Built on top of Orchestra Testbench, it offers a fully configured Laravel testing environment with sample data, automatic cleanup, and testing utilities specifically designed for Streams development.
## What's Included
- **Pre-configured Laravel Environment**: A complete Laravel application setup ready for testing
- **Sample Test Data**: Star Wars-themed sample streams for learning and testing
- **Automatic Data Management**: Automatic backup and restoration of test data between tests
- **Base TestCase**: A custom TestCase class with Streams-specific utilities
- **PHPUnit 10 Support**: Modern testing framework integration
## Quick Example
```php
get();
$this->assertCount(7, $films);
}
}
```
## Who Should Use This Package?
- **Streams Developers**: Anyone building applications with the Streams platform
- **Package Authors**: Developers creating Streams addons and extensions
- **Teams**: Organizations needing reliable testing infrastructure for Streams projects
- **Learners**: Those exploring Streams capabilities in a safe testing environment
## Key Benefits
### 1. Zero Configuration
The package comes pre-configured with a Laravel application, streams definitions, and sample data. You can start writing tests immediately without setup hassle.
### 2. Clean State Testing
Every test runs with a fresh copy of the sample data. The TestCase automatically restores data after each test, ensuring no test pollution.
### 3. Real-World Examples
Sample streams include films, people, planets, and more—providing realistic data structures to learn from and test against.
### 4. Laravel Integration
Full access to Laravel's testing features, including database assertions, HTTP testing, and more.
## Getting Started
Ready to start testing? Continue to the [Installation Guide](/docs/testing/installation) to set up the Streams Testing package in your project.
---
# Testing reference: Installation
> Install and configure the Streams Testing package.
Source: https://streams.dev/docs/testing/installation
This guide will walk you through installing and configuring the Streams Testing package for your project.
## Requirements
Before installing, ensure your environment meets these requirements:
- **PHP**: 8.2 or higher (required by Streams Core)
- **Laravel**: 10.x only for now. The package requires `orchestra/testbench ^8.36`, which targets Laravel 10. Support is being widened to `^8.36|^9.15|^10.8` (Laravel 10, 11, and 12). Until then you can't install it in a Laravel 11 or 12 app; streams.dev, which runs Laravel 12, doesn't use it for that reason. See the [version matrix](/docs/installation#version-matrix).
- **Streams Core**: ^2.0
- **Composer**: Latest version recommended
## Installing the Package
Install the Streams Testing package via Composer as a development dependency:
```bash
composer require --dev streams/testing:1.0.x-dev
```
This will install the package and all its dependencies, including Orchestra Testbench and PHPUnit.
## Verifying Installation
After installation, verify everything is set up correctly:
```bash
vendor/bin/phpunit --version
```
You should see PHPUnit 10.x listed.
## Basic Configuration
### 1. Create phpunit.xml
If you don't already have a `phpunit.xml` file in your project root, create one:
```xml
./tests./src
```
### 2. Create Tests Directory
Create a `tests` directory if it doesn't exist:
```bash
mkdir tests
```
### 3. Write Your First Test
Create a test file `tests/ExampleTest.php`:
```php
assertTrue(true);
}
public function test_streams_available()
{
$films = Streams::entries('films');
$this->assertNotNull($films);
}
}
```
### 4. Run Your Tests
Execute your test suite:
```bash
vendor/bin/phpunit
```
You should see output indicating your tests passed:
```text
PHPUnit 10.5.58 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.123, Memory: 24.00 MB
OK (2 tests, 2 assertions)
```
## Advanced Configuration
### Custom Test Namespace
Update your `composer.json` to use a custom test namespace:
```json
{
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
```
Then run:
```bash
composer dump-autoload
```
### IDE Configuration
#### PHPStorm / IntelliJ IDEA
1. Go to **Settings** → **PHP** → **Test Frameworks**
2. Click **+** and select **PHPUnit Local**
3. Set **Path to phpunit.phar** to `vendor/bin/phpunit`
4. Choose **Use Composer autoloader**
5. Set **Path to script** to `vendor/autoload.php`
#### VS Code
Install the "PHPUnit" extension and add to `.vscode/settings.json`:
```json
{
"phpunit.php": "/usr/local/bin/php",
"phpunit.phpunit": "vendor/bin/phpunit",
"phpunit.args": [
"--colors=always"
]
}
```
### CI/CD Integration
#### GitHub Actions
Create `.github/workflows/tests.yml`:
```yaml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: dom, curl, libxml, mbstring, zip
coverage: none
- name: Install Dependencies
run: composer install --prefer-dist --no-interaction
- name: Run Tests
run: vendor/bin/phpunit
```
## Troubleshooting
### "Class not found" Errors
Regenerate the autoloader:
```bash
composer dump-autoload
```
### Memory Limit Issues
Increase PHP memory limit in `phpunit.xml`:
```xml
```
### Permission Errors
Ensure test data directories are writable:
```bash
chmod -R 775 vendor/streams/testing/laravel/streams
```
## Next Steps
Now that you have the package installed, learn about:
- [Test Data](/docs/testing/test-data) — sample streams
- [Writing Tests](/docs/testing/writing-tests)
- [Configuration](/docs/testing/configuration)
---
# Testing reference: Test Data
> Sample streams and test data included in the package.
Source: https://streams.dev/docs/testing/test-data
The Streams Testing package includes a rich set of sample data based on the Star Wars universe. This data provides realistic examples for testing and learning Streams concepts.
## Available Streams
The package includes these pre-configured streams:
### Films
**Stream**: `films`
**Entries**: 7 Star Wars films
**Key Field**: `episode_id`
```php
$films = Streams::entries('films')->get();
// Returns 7 film entries
```
#### Example Entry
```json
{
"episode_id": 1,
"title": "A New Hope",
"director": "George Lucas",
"producer": "Gary Kurtz, Rick McCallum",
"release_date": "1977-05-25",
"opening_crawl": "It is a period of civil war..."
}
```
#### Fields
- `episode_id` (integer) - Unique episode number
- `title` (string) - Film title
- `director` (string) - Director name
- `producer` (string) - Producer names
- `release_date` (datetime) - Release date
- `opening_crawl` (text) - Opening text
- `created` (datetime) - Creation timestamp
- `edited` (datetime) - Last edit timestamp
### People
**Stream**: `people`
**Entries**: Multiple characters
**Key Field**: `id`
```php
$characters = Streams::entries('people')
->where('homeworld', 'Tatooine')
->get();
```
#### Example Entry
```json
{
"id": 1,
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "Tatooine"
}
```
### Planets
**Stream**: `planets`
**Entries**: Various planets
**Key Field**: `id`
```php
$planets = Streams::entries('planets')
->where('climate', 'like', '%arid%')
->get();
```
#### Example Entry
```json
{
"id": 1,
"name": "Tatooine",
"rotation_period": "23",
"orbital_period": "304",
"diameter": "10465",
"climate": "arid",
"gravity": "1 standard",
"terrain": "desert",
"population": "200000"
}
```
### Species
**Stream**: `species`
**Entries**: Various species
**Key Field**: `id`
```php
$species = Streams::entries('species')
->where('classification', 'mammal')
->get();
```
### Starships
**Stream**: `starships`
**Entries**: Various starships
**Key Field**: `id`
```php
$starships = Streams::entries('starships')
->where('manufacturer', 'like', '%Incom%')
->get();
```
### Vehicles
**Stream**: `vehicles`
**Entries**: Various vehicles
**Key Field**: `id`
```php
$vehicles = Streams::entries('vehicles')
->orderBy('cost_in_credits', 'desc')
->get();
```
### Files
**Stream**: `files`
**Entries**: File handling examples
**Key Field**: `id`
```php
$files = Streams::entries('files')->get();
```
## Using Test Data
### Querying Data
```php
public function test_query_films_by_director()
{
$films = Streams::entries('films')
->where('director', 'George Lucas')
->get();
$this->assertGreaterThan(0, $films->count());
}
```
### Accessing Relationships
```php
public function test_character_homeworld()
{
$luke = Streams::entries('people')
->where('name', 'Luke Skywalker')
->first();
$this->assertEquals('Tatooine', $luke->homeworld);
}
```
### Counting Entries
```php
public function test_all_films_present()
{
$count = Streams::entries('films')->count();
$this->assertEquals(7, $count);
}
```
### Sorting Data
```php
public function test_films_chronological_order()
{
$films = Streams::entries('films')
->orderBy('release_date')
->get();
$this->assertEquals('A New Hope', $films->first()->title);
}
```
## Data Management
### Automatic Restoration
The test data is automatically restored after each test. The TestCase handles this in the `tearDown()` method:
```php
protected function tearDown(): void
{
$this->restoreStreamsData();
parent::tearDown();
}
```
This ensures:
- Each test starts with clean data
- Tests don't interfere with each other
- You can modify data without permanent changes
### Manual Restoration
If needed, you can manually restore data:
```php
public function test_with_manual_restore()
{
// Modify some data
Streams::make('films')->create([...]);
// Manually restore
$this->restoreStreamsData();
// Data is back to original state
$this->assertCount(7, Streams::entries('films')->get());
}
```
### Data Location
Test data is stored in:
- **Active**: `vendor/streams/testing/laravel/streams/`
- **Backup**: `vendor/streams/testing/laravel/streams.bak/`
## Custom Test Data
### Adding Your Own Streams
Create custom stream definitions in your test setup:
```php
protected function setUp(): void
{
parent::setUp();
// Define a custom stream for testing
Streams::build([
'id' => 'custom_products',
'name' => 'Products',
'source' => [
'type' => 'file',
'format' => 'json',
],
'fields' => [
'id' => 'integer',
'name' => 'string',
'price' => 'decimal',
],
])->save();
}
```
### Loading Custom Fixtures
Load your own test data:
```php
public function test_with_custom_data()
{
$products = [
['id' => 1, 'name' => 'Widget', 'price' => 9.99],
['id' => 2, 'name' => 'Gadget', 'price' => 19.99],
];
foreach ($products as $product) {
Streams::make('custom_products')->create($product);
}
$this->assertCount(2, Streams::entries('custom_products')->get());
}
```
## Best Practices
### 1. Don't Rely on Specific IDs
IDs may change. Use attributes instead:
```php
// Bad
$film = Streams::entries('films')->find(1);
// Good
$film = Streams::entries('films')
->where('title', 'A New Hope')
->first();
```
### 2. Use Factories for Complex Data
Create factories for reusable test data:
```php
class TestHelpers
{
public static function createFilm($attributes = [])
{
return Streams::make('films')->create(array_merge([
'title' => 'Test Film',
'director' => 'Test Director',
'release_date' => now(),
], $attributes));
}
}
```
### 3. Test Data Integrity
Verify expected data exists:
```php
protected function setUp(): void
{
parent::setUp();
$this->assertGreaterThan(0, Streams::entries('films')->count(),
'Test data not loaded properly');
}
```
## Next Steps
- [Writing Tests](/docs/testing/writing-tests)
- [Configuration](/docs/testing/configuration)
- [Troubleshooting](/docs/testing/troubleshooting)
---
# Testing reference: Writing Tests
> Write effective tests for Streams applications.
Source: https://streams.dev/docs/testing/writing-tests
This guide covers everything you need to know about writing tests for Streams applications, from basic examples to advanced testing patterns.
## Basic Test Structure
### Your First Test
Every test extends the `Streams\Testing\TestCase` class:
```php
assertTrue(true);
}
}
```
### Anatomy of a Test
```php
public function test_descriptive_name() // 1. Test method
{
// 2. Arrange - Set up test data
$stream = Streams::make('films');
// 3. Act - Perform the action
$result = $stream->entries()->count();
// 4. Assert - Verify the result
$this->assertEquals(7, $result);
}
```
## Testing Streams
### Creating Entries
```php
public function test_creates_new_film()
{
$film = Streams::make('films')->create([
'title' => 'New Film',
'director' => 'New Director',
'release_date' => '2025-01-01',
]);
$this->assertNotNull($film->id);
$this->assertEquals('New Film', $film->title);
}
```
### Reading Entries
```php
public function test_finds_film_by_title()
{
$film = Streams::entries('films')
->where('title', 'A New Hope')
->first();
$this->assertNotNull($film);
$this->assertEquals('George Lucas', $film->director);
}
```
### Updating Entries
```php
public function test_updates_film_director()
{
$film = Streams::entries('films')
->where('title', 'A New Hope')
->first();
$film->update(['director' => 'Updated Director']);
$updated = Streams::entries('films')
->where('title', 'A New Hope')
->first();
$this->assertEquals('Updated Director', $updated->director);
}
```
### Deleting Entries
```php
public function test_deletes_film()
{
$initialCount = Streams::entries('films')->count();
$film = Streams::entries('films')->first();
$film->delete();
$finalCount = Streams::entries('films')->count();
$this->assertEquals($initialCount - 1, $finalCount);
}
```
## Testing Queries
### Simple Where Clauses
```php
public function test_filters_by_director()
{
$films = Streams::entries('films')
->where('director', 'George Lucas')
->get();
$this->assertGreaterThan(0, $films->count());
foreach ($films as $film) {
$this->assertEquals('George Lucas', $film->director);
}
}
```
### Complex Queries
```php
public function test_complex_film_query()
{
$films = Streams::entries('films')
->where('director', 'George Lucas')
->where('release_date', '>=', '1977-01-01')
->orderBy('release_date')
->limit(3)
->get();
$this->assertLessThanOrEqual(3, $films->count());
}
```
### Using Like Operator
```php
public function test_searches_film_titles()
{
$films = Streams::entries('films')
->where('title', 'like', '%Empire%')
->get();
$this->assertGreaterThan(0, $films->count());
}
```
### Counting Results
```php
public function test_counts_films_by_director()
{
$count = Streams::entries('films')
->where('director', 'George Lucas')
->count();
$this->assertGreaterThan(0, $count);
}
```
## Testing Stream Definitions
### Validating Stream Configuration
```php
public function test_films_stream_configuration()
{
$stream = Streams::make('films');
$this->assertEquals('Films', $stream->name);
$this->assertEquals('episode_id', $stream->config['key_name']);
}
```
### Testing Field Definitions
```php
public function test_films_has_required_fields()
{
$stream = Streams::make('films');
$fields = $stream->fields;
$this->assertArrayHasKey('title', $fields);
$this->assertArrayHasKey('director', $fields);
$this->assertArrayHasKey('release_date', $fields);
}
```
### Validating Field Types
```php
public function test_field_types()
{
$stream = Streams::make('films');
$this->assertEquals('integer', $stream->fields['episode_id']->type);
$this->assertEquals('string', $stream->fields['title']->type);
$this->assertEquals('datetime', $stream->fields['release_date']->type);
}
```
## Testing Relationships
### One-to-Many Relationships
```php
public function test_planet_has_residents()
{
$tatooine = Streams::entries('planets')
->where('name', 'Tatooine')
->first();
$residents = Streams::entries('people')
->where('homeworld', $tatooine->name)
->get();
$this->assertGreaterThan(0, $residents->count());
}
```
### Many-to-Many Relationships
```php
public function test_character_appears_in_films()
{
$luke = Streams::entries('people')
->where('name', 'Luke Skywalker')
->first();
// Assuming a films relationship
if (isset($luke->films)) {
$this->assertGreaterThan(0, count($luke->films));
}
}
```
## Using Setup and Teardown
### Setup Method
Run code before each test:
```php
protected function setUp(): void
{
parent::setUp();
// Create test data that all tests need
$this->testFilm = Streams::make('films')->create([
'title' => 'Test Film',
'director' => 'Test Director',
]);
}
```
### Teardown Method
The parent TestCase handles data restoration automatically. Add custom cleanup if needed:
```php
protected function tearDown(): void
{
// Custom cleanup here
parent::tearDown(); // This restores test data
}
```
## Data Providers
Test the same logic with different data:
```php
/**
* @dataProvider filmDirectorProvider
*/
public function test_finds_films_by_director($director, $expectedMinimum)
{
$films = Streams::entries('films')
->where('director', $director)
->get();
$this->assertGreaterThanOrEqual($expectedMinimum, $films->count());
}
public static function filmDirectorProvider()
{
return [
'George Lucas' => ['George Lucas', 1],
'Irvin Kershner' => ['Irvin Kershner', 1],
'Richard Marquand' => ['Richard Marquand', 1],
];
}
```
## Testing Exceptions
### Expecting Exceptions
```php
public function test_throws_exception_for_invalid_stream()
{
$this->expectException(\Exception::class);
Streams::make('nonexistent_stream');
}
```
### Testing Validation Errors
```php
public function test_validates_required_fields()
{
$this->expectException(\Illuminate\Validation\ValidationException::class);
Streams::make('films')->create([
// Missing required 'title' field
'director' => 'Test Director',
]);
}
```
## Common Assertions
### Equality Assertions
```php
$this->assertEquals($expected, $actual);
$this->assertNotEquals($unexpected, $actual);
$this->assertSame($expected, $actual); // Strict comparison
```
### Boolean Assertions
```php
$this->assertTrue($condition);
$this->assertFalse($condition);
$this->assertNull($value);
$this->assertNotNull($value);
```
### Collection Assertions
```php
$this->assertCount($expectedCount, $collection);
$this->assertEmpty($collection);
$this->assertNotEmpty($collection);
$this->assertContains($needle, $collection);
```
### Array Assertions
```php
$this->assertArrayHasKey($key, $array);
$this->assertArrayNotHasKey($key, $array);
$this->assertIsArray($value);
```
### String Assertions
```php
$this->assertStringContainsString($needle, $haystack);
$this->assertStringStartsWith($prefix, $string);
$this->assertStringEndsWith($suffix, $string);
$this->assertMatchesRegularExpression($pattern, $string);
```
### Numeric Assertions
```php
$this->assertGreaterThan($expected, $actual);
$this->assertGreaterThanOrEqual($expected, $actual);
$this->assertLessThan($expected, $actual);
$this->assertLessThanOrEqual($expected, $actual);
```
## Testing Best Practices
### 1. One Assertion Per Test (When Possible)
```php
// Good
public function test_film_has_title()
{
$film = Streams::entries('films')->first();
$this->assertNotEmpty($film->title);
}
public function test_film_has_director()
{
$film = Streams::entries('films')->first();
$this->assertNotEmpty($film->director);
}
```
### 2. Use Descriptive Test Names
```php
// Bad
public function test_film() { }
// Good
public function test_creates_film_with_all_required_fields() { }
```
### 3. Follow the AAA Pattern
```php
public function test_updates_film_release_date()
{
// Arrange
$film = Streams::entries('films')->first();
$newDate = '2025-12-25';
// Act
$film->update(['release_date' => $newDate]);
// Assert
$this->assertEquals($newDate, $film->fresh()->release_date);
}
```
### 4. Test Edge Cases
```php
public function test_handles_empty_query_results()
{
$films = Streams::entries('films')
->where('director', 'Nonexistent Director')
->get();
$this->assertCount(0, $films);
}
```
### 5. Keep Tests Independent
Each test should work in isolation:
```php
public function test_first_test()
{
$film = Streams::make('films')->create(['title' => 'Test']);
$this->assertNotNull($film);
}
public function test_second_test()
{
// Don't rely on data from test_first_test
$count = Streams::entries('films')->count();
$this->assertEquals(7, $count); // Original test data
}
```
## Debugging Tests
### Using dump() and dd()
```php
public function test_debugging_example()
{
$films = Streams::entries('films')->get();
dump($films); // Output without stopping
// dd($films); // Dump and die
$this->assertNotEmpty($films);
}
```
### Verbose Output
Run tests with verbose output:
```bash
vendor/bin/phpunit --testdox --verbose
```
### Running Single Tests
Test specific methods:
```bash
vendor/bin/phpunit --filter test_specific_method
```
## Next Steps
- [Configuration](/docs/testing/configuration)
- [Test Data](/docs/testing/test-data)
- [Troubleshooting](/docs/testing/troubleshooting)
---
# Testing reference: Configuration
> Configure the Streams Testing environment.
Source: https://streams.dev/docs/testing/configuration
Learn how to configure the Streams Testing package to match your project's needs, from basic PHPUnit settings to advanced test environment customization.
## PHPUnit Configuration
### Basic phpunit.xml
The minimal configuration for Streams Testing:
```xml
./tests
```
### Recommended Configuration
A more comprehensive setup with best practices:
```xml
./tests/Unit./tests/Feature./src./src/Console./src/TestServiceProvider.php
```
### Configuration Options Explained
#### Test Execution
```xml
stopOnError="false"
stopOnFailure="false"
stopOnWarning="false"
stopOnRisky="false"
beStrictAboutOutputDuringTests="true"
>
```
#### Failure Handling
```xml
failOnRisky="true"
failOnDeprecation="false"
failOnNotice="false"
>
```
#### Display Options
```xml
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerNotices="true"
>
```
## Environment Variables
### Laravel Configuration
Configure Laravel behavior during tests:
```xml
```
### Streams Configuration
Configure Streams-specific settings:
```xml
```
### PHP Settings
Adjust PHP behavior:
```xml
```
## Custom TestCase
### Extending the Base TestCase
Create your own base TestCase with custom functionality:
```php
artisan('cache:clear');
}
protected function tearDown(): void
{
// Custom cleanup
parent::tearDown();
}
/**
* Create a test film entry
*/
protected function createFilm(array $attributes = []): object
{
return Streams::make('films')->create(array_merge([
'title' => 'Test Film',
'director' => 'Test Director',
'release_date' => now()->format('Y-m-d'),
], $attributes));
}
}
```
### Using Your Custom TestCase
```php
createFilm(['title' => 'My Film']);
$this->assertEquals('My Film', $film->title);
}
}
```
## Test Organization
### Directory Structure
Organize tests by type:
```text
tests/
├── Feature/ # Integration tests
│ ├── FilmTest.php
│ └── PlanetTest.php
├── Unit/ # Unit tests
│ ├── StreamTest.php
│ └── EntryTest.php
├── Fixtures/ # Test data
│ └── films.json
└── Support/ # Test helpers
└── Helpers.php
```
### Autoloading Test Helpers
Add to `composer.json`:
```json
{
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
},
"files": [
"tests/Support/helpers.php"
]
}
}
```
## Code Coverage
### Enable Coverage
Install Xdebug or PCOV, then configure coverage:
```xml
./src./src/Console./src/TestServiceProvider.php
```
### Generate Coverage Report
```bash
# HTML report
vendor/bin/phpunit --coverage-html coverage
# Text report
vendor/bin/phpunit --coverage-text
# Clover XML (for CI)
vendor/bin/phpunit --coverage-clover coverage.xml
```
## Test Filtering
### Run Specific Test Suites
```bash
# Run only unit tests
vendor/bin/phpunit --testsuite Unit
# Run only feature tests
vendor/bin/phpunit --testsuite Feature
```
### Run Tests by Group
Add groups to tests:
```php
/**
* @group films
* @group integration
*/
public function test_film_creation()
{
// Test code
}
```
Run by group:
```bash
# Run only films tests
vendor/bin/phpunit --group films
# Exclude slow tests
vendor/bin/phpunit --exclude-group slow
```
### Run Tests by Filter
```bash
# Run tests matching pattern
vendor/bin/phpunit --filter FilmTest
# Run specific test method
vendor/bin/phpunit --filter test_creates_film
```
## Performance Optimization
### Parallel Test Execution
Install ParaTest:
```bash
composer require --dev brianium/paratest
```
Run tests in parallel:
```bash
vendor/bin/paratest --processes=4
```
### Process Isolation
For tests that need isolation:
```xml
```
Or per-test:
```php
/**
* @runInSeparateProcess
*/
public function test_isolated()
{
// Test code
}
```
### Cache Configuration
Enable result caching:
```xml
```
Add to `.gitignore`:
```text
.phpunit.cache/
coverage/
```
## Database Configuration
### SQLite In-Memory
Fast testing with SQLite:
```xml
```
### MySQL Testing Database
```xml
```
## Continuous Integration
### GitHub Actions
`.github/workflows/tests.yml`:
```yaml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: [8.2, 8.3]
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip
coverage: xdebug
- name: Install Dependencies
run: composer install --prefer-dist --no-interaction
- name: Run Tests
run: vendor/bin/phpunit --coverage-clover coverage.xml
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
```
### GitLab CI
`.gitlab-ci.yml`:
```yaml
test:
image: php:8.2
before_script:
- apt-get update -y
- apt-get install -y git unzip
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
script:
- vendor/bin/phpunit
artifacts:
reports:
junit: phpunit-report.xml
```
## Next Steps
- [Writing Tests](/docs/testing/writing-tests)
- [Troubleshooting](/docs/testing/troubleshooting)
---
# Testing reference: Troubleshooting
> Common issues when testing with Streams Testing.
Source: https://streams.dev/docs/testing/troubleshooting
This guide helps you resolve common issues when using the Streams Testing package.
## Installation Issues
### Class Not Found
**Problem**: `Class 'Streams\Testing\TestCase' not found`
**Solution**:
```bash
# Regenerate autoload files
composer dump-autoload
# Clear any cached autoload files
rm -rf vendor/composer
composer install
```
### PHPUnit Not Found
**Problem**: `vendor/bin/phpunit: No such file or directory`
**Solution**:
```bash
# Reinstall dev dependencies
composer install --dev
# Or explicitly require PHPUnit
composer require --dev phpunit/phpunit
```
### Orchestra Testbench Missing
**Problem**: `Class 'Orchestra\Testbench\TestCase' not found`
**Solution**:
```bash
# Install compatible version
composer require --dev orchestra/testbench:^8.0
```
## Configuration Issues
### No Tests Found
**Problem**: PHPUnit reports `No tests executed!`
**Solutions**:
1. **Check phpunit.xml test suite configuration**:
```xml
./tests
```
2. **Verify test file naming**:
- Files must end with `Test.php` (e.g., `FilmTest.php`)
- Test methods must start with `test` or use `@test` annotation
3. **Check test directory exists**:
```bash
ls -la tests/
```
### Wrong PHPUnit Version
**Problem**: PHPUnit schema errors or deprecated features
**Solution**:
Check PHPUnit version matches configuration:
```bash
vendor/bin/phpunit --version
```
Update `phpunit.xml` schema:
```xml
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
```
### Missing APP_KEY
**Problem**: `No application encryption key has been specified`
**Solution**:
Add to `phpunit.xml`:
```xml
```
Or generate a new one:
```bash
php artisan key:generate --show
```
## Test Execution Issues
### Memory Limit Exceeded
**Problem**: `Fatal error: Allowed memory size exhausted`
**Solutions**:
1. **Increase memory limit in phpunit.xml**:
```xml
```
2. **Run with more memory**:
```bash
php -d memory_limit=512M vendor/bin/phpunit
```
3. **Use process isolation**:
```xml
```
### Tests Timeout
**Problem**: Tests hang or timeout
**Solutions**:
1. **Increase max execution time**:
```xml
```
2. **Check for infinite loops**:
```php
// Add timeouts to potentially long operations
$films = Streams::entries('films')
->timeout(30)
->get();
```
3. **Run with verbose output to identify hanging test**:
```bash
vendor/bin/phpunit --verbose --debug
```
### Permission Denied Errors
**Problem**: `Permission denied` when accessing test data
**Solution**:
```bash
# Make streams directories writable
chmod -R 775 vendor/streams/testing/laravel/streams
chmod -R 775 vendor/streams/testing/laravel/streams.bak
# Or change ownership
sudo chown -R $USER:$USER vendor/streams/testing/laravel/streams
```
## Test Data Issues
### Test Data Not Loading
**Problem**: Sample streams return empty results
**Solutions**:
1. **Verify test data exists**:
```bash
ls -la vendor/streams/testing/laravel/streams/
```
2. **Check backup data**:
```bash
ls -la vendor/streams/testing/laravel/streams.bak/
```
3. **Manually restore data**:
```php
public function setUp(): void
{
parent::setUp();
$this->restoreStreamsData();
}
```
4. **Verify stream files are valid JSON**:
```bash
php -r "json_decode(file_get_contents('vendor/streams/testing/laravel/streams/films.json'));"
```
### Data Not Resetting Between Tests
**Problem**: Modified data persists across tests
**Solutions**:
1. **Ensure parent tearDown is called**:
```php
protected function tearDown(): void
{
// Your cleanup code here
parent::tearDown(); // Must call this!
}
```
2. **Check file permissions**:
```bash
# Ensure test can write and delete
chmod -R 755 vendor/streams/testing/laravel/streams
```
3. **Manually verify restoration**:
```php
public function test_data_resets()
{
$initialCount = Streams::entries('films')->count();
Streams::make('films')->create(['title' => 'New Film']);
$this->restoreStreamsData();
$finalCount = Streams::entries('films')->count();
$this->assertEquals($initialCount, $finalCount);
}
```
### Invalid JSON in Stream Files
**Problem**: `JSON decode error` when loading streams
**Solution**:
Validate and fix JSON:
```bash
# Validate JSON
cat vendor/streams/testing/laravel/streams/films.json | python -m json.tool
# Or use jq
jq . vendor/streams/testing/laravel/streams/films.json
```
## Assertion Issues
### Unexpected Test Failures
**Problem**: Tests fail unexpectedly
**Debug Steps**:
1. **Add debug output**:
```php
public function test_debug_example()
{
$films = Streams::entries('films')->get();
dump($films->count()); // Check actual count
dd($films->toArray()); // Inspect full data
$this->assertCount(7, $films);
}
```
2. **Use verbose assertions**:
```php
$this->assertEquals(
$expected,
$actual,
"Expected $expected but got $actual"
);
```
3. **Run single test**:
```bash
vendor/bin/phpunit --filter test_specific_method --testdox
```
### Type Comparison Issues
**Problem**: `Expected integer but got string`
**Solution**:
Use appropriate assertions:
```php
// Loose comparison
$this->assertEquals(7, $count);
// Strict comparison
$this->assertSame(7, $count);
// Type checking
$this->assertIsInt($count);
$this->assertEquals(7, (int) $count);
```
### Collection vs Array Confusion
**Problem**: Assertions fail on collections
**Solution**:
Convert collections appropriately:
```php
// Get collection
$films = Streams::entries('films')->get();
// For counting
$this->assertCount(7, $films);
// For array assertions
$this->assertIsArray($films->toArray());
// For iteration
foreach ($films as $film) {
$this->assertNotNull($film);
}
```
## Laravel Integration Issues
### Route Not Found in Tests
**Problem**: `Route [name] not defined`
**Solution**:
Ensure routes are loaded in TestCase:
```php
protected function getPackageProviders($app)
{
return [
\Your\Package\ServiceProvider::class,
];
}
```
### Config Not Loading
**Problem**: Configuration values not available
**Solution**:
Define config in TestCase:
```php
protected function defineEnvironment($app)
{
$app['config']->set('streams.path', __DIR__ . '/streams');
}
```
### Service Provider Not Loaded
**Problem**: Services not registered
**Solution**:
Register providers explicitly:
```php
protected function getPackageProviders($app)
{
return [
\Streams\Core\StreamsServiceProvider::class,
\Streams\Testing\TestServiceProvider::class,
];
}
```
## IDE Issues
### PHPStorm Not Running Tests
**Problem**: Can't run tests from IDE
**Solutions**:
1. **Configure PHPUnit**:
- Settings → PHP → Test Frameworks
- Add PHPUnit Local
- Use Composer autoloader: `vendor/autoload.php`
2. **Mark directories**:
- Right-click `tests/` → Mark Directory As → Test Sources Root
3. **Refresh configuration**:
- File → Invalidate Caches / Restart
### VS Code Not Recognizing Tests
**Problem**: Test runner doesn't find tests
**Solutions**:
1. **Install PHPUnit extension**:
- Better PHPUnit by calebporzio
2. **Configure settings.json**:
```json
{
"phpunit.phpunit": "vendor/bin/phpunit",
"phpunit.args": ["--colors=always"]
}
```
3. **Reload window**:
- Cmd/Ctrl + Shift + P → Reload Window
## Performance Issues
### Tests Running Slowly
**Solutions**:
1. **Use process isolation sparingly**:
```xml
```
2. **Disable coverage when not needed**:
```bash
vendor/bin/phpunit --no-coverage
```
3. **Run tests in parallel**:
```bash
composer require --dev brianium/paratest
vendor/bin/paratest
```
4. **Use SQLite in-memory for database tests**:
```xml
```
### High Memory Usage
**Solutions**:
1. **Clear data after tests**:
```php
protected function tearDown(): void
{
$this->clearData();
parent::tearDown();
}
```
2. **Use fewer fixtures**:
```php
// Load only needed data
$this->loadFixtures(['films']); // Not all streams
```
3. **Garbage collection**:
```php
protected function tearDown(): void
{
gc_collect_cycles();
parent::tearDown();
}
```
## Getting Help
### Diagnostic Information
When reporting issues, include:
```bash
# PHP version
php -v
# Composer info
composer show
# PHPUnit version
vendor/bin/phpunit --version
# Streams core version
composer show streams/core
# Run tests with verbose output
vendor/bin/phpunit --verbose --debug
```
### Enable Debug Mode
Add to `phpunit.xml`:
```xml
```
### Community Resources
- **Documentation**: https://streams.dev/docs/testing/introduction
- **GitHub Issues**: https://github.com/laravel-streams/streams-testing/issues
- **Discord**: Join the Streams community
- **Stack Overflow**: Tag with `laravel-streams`
## Quick Checklist
When tests aren't working, check:
- [ ] `composer dump-autoload` executed
- [ ] `phpunit.xml` exists and is valid
- [ ] Test files end with `Test.php`
- [ ] Test methods start with `test`
- [ ] Extending `\Streams\Testing\TestCase`
- [ ] `parent::setUp()` and `parent::tearDown()` called
- [ ] File permissions correct on stream data
- [ ] APP_KEY set in phpunit.xml
- [ ] Correct PHP version (8.2+)
- [ ] Dependencies installed with `composer install`
Still having issues? Check the [GitHub issues](https://github.com/laravel-streams/streams-testing/issues) or create a new one with the diagnostic information above.
---
# Client reference: Introduction
> A zero-dependency JavaScript client for the Streams REST API.
Source: https://streams.dev/docs/client/introduction
`@laravel-streams/api-client` is a JavaScript client for the [Streams API](/docs/api/introduction). It wraps the API's stream and entry endpoints in a small fluent interface, with a Laravel-style criteria builder and a request/response middleware pipeline.
## What it is
- **Zero runtime dependencies.** It uses the native `fetch` API, and ships ESM (`dist/module.js`) and CommonJS (`dist/index.js`) builds.
- **Two resources.** `client.streams` for stream definitions and `client.entries` for entry CRUD.
- **Criteria builder.** `where`, `orWhere`, `orderBy`, `limit`, and `paginate`, compiled into the API's query parameters.
- **Middleware.** Request, response, and error hooks with priorities. The defaults serialize request bodies, compile criteria, append query strings, and unwrap the response envelope.
It is the JavaScript counterpart to the PHP [Streams SDK](/docs/sdk/introduction), but they are different things: the SDK is Artisan tooling for building a Streams app, and the client is a runtime library for consuming a Streams API from a browser or Node.
## Why it exists
The Streams API describes itself: every response carries links to related resources, and stream definitions are available over HTTP. The client lets front ends and Node services use that API without hand-writing `fetch` calls, query-string encoding, or envelope parsing, and it keeps the query vocabulary (`where`, `orderBy`, `paginate`) the same as the [criteria](/docs/core/criteria) you already use in PHP.
## How to use it
Install from npm (the current release is **3.0.0**):
```bash
npm install @laravel-streams/api-client
```
Point the client at your API prefix and query a stream:
```javascript
import { Client, Criteria } from '@laravel-streams/api-client';
const client = new Client({ baseURL: 'https://example.com/api' });
const streams = await client.streams.get();
const posts = await client.entries.get('posts', {
criteria: new Criteria()
.where('status', 'published')
.orderBy('created_at', 'desc')
.limit(10),
});
await client.entries.patch('posts', 1, { title: 'Updated title' });
```
The server side needs [streams/api](/docs/api/installation) installed and enabled. The API has no built-in authentication; your app decides who can call it (see [API authentication](/docs/api/authentication)).
## How to extend it
Add behavior by writing a middleware class and registering it with `client.use()`:
```javascript
import { Middleware } from '@laravel-streams/api-client';
class TenantHeader extends Middleware {
async request(request, client) {
request.headers.set('X-Tenant', 'acme');
return request;
}
}
client.use(new TenantHeader());
```
`AuthorizationMiddleware` sets a bearer token for you. See [Middleware](/docs/client/middleware) for priorities and the error hook.
## Where to next
- [Installation](/docs/client/installation)
- [Quick start](/docs/client/quickstart)
- [Client configuration](/docs/client/client)
- [Criteria query builder](/docs/client/criteria)
---
# Client reference: Installation
> Installing and setting up the Streams API Client
Source: https://streams.dev/docs/client/installation
## Requirements
- Node.js 14.0 or higher
- npm, yarn, or pnpm
## Installing via NPM
Install the package using your preferred package manager:
### NPM
```bash
npm install @laravel-streams/api-client
```
### Yarn
```bash
yarn add @laravel-streams/api-client
```
### PNPM
```bash
pnpm add @laravel-streams/api-client
```
## Installing from Source
### Cloning with Git
```bash
git clone git@github.com:laravel-streams/api-client.git
cd api-client
npm install
npm run build
```
### Building from Source
```bash
# Install dependencies
npm install
# Build the distribution files
npm run build
# Run tests to verify
npm test
```
## Package Contents
After installation, the package includes:
```text
node_modules/@laravel-streams/api-client/
├── dist/
│ ├── index.js # CommonJS build
│ └── module.js # ES Module build
├── src/
│ ├── Client.js
│ ├── Criteria.js
│ ├── Entries.js
│ ├── Streams.js
│ └── middleware/
└── package.json
```
## Importing the Client
### ES Modules (Recommended)
```javascript
import { Client, Criteria } from '@laravel-streams/api-client';
```
### CommonJS
```javascript
const { Client, Criteria } = require('@laravel-streams/api-client');
```
### Browser (ES Modules)
```html
```
## Updating
Update to the latest version:
```bash
npm update @laravel-streams/api-client
```
Or update to a specific version:
```bash
npm install @laravel-streams/api-client@3.0.0
```
## Verifying Installation
Create a simple test file to verify the installation:
```javascript
// test-install.mjs
import { Client, Criteria } from '@laravel-streams/api-client';
console.log('✓ Client imported:', typeof Client);
console.log('✓ Criteria imported:', typeof Criteria);
const client = new Client({ baseURL: 'http://localhost/api' });
console.log('✓ Client created successfully');
```
Run it:
```bash
node test-install.mjs
```
Expected output:
```text
✓ Client imported: function
✓ Criteria imported: function
✓ Client created successfully
```
## Next Steps
- [Quick Start Guide](/docs/client/quickstart) - Get up and running quickly
- [Client Configuration](/docs/client/client) - Configure the client for your needs
- [Working with Entries](/docs/client/entries) - Start interacting with your data
---
# Client reference: Quick Start
> Get started with the Streams API Client in minutes
Source: https://streams.dev/docs/client/quickstart
Get up and running with the Streams API Client in just a few minutes.
## Basic Setup
```javascript
import { Client } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'http://localhost/api'
});
```
## Working with Streams
### Get All Streams
```javascript
const response = await client.streams.get();
console.log(response.data); // Array of streams
```
### Get a Single Stream
```javascript
const response = await client.streams.find('posts');
console.log(response.data); // Stream object
```
## Working with Entries
### Get Entries
```javascript
// Get all entries from a stream
const response = await client.entries.get('posts');
console.log(response.data); // Array of entries
```
### Find a Specific Entry
```javascript
const response = await client.entries.find('posts', 1);
console.log(response.data); // Single entry object
```
### Create an Entry
```javascript
const response = await client.entries.post('posts', {
title: 'My First Post',
content: 'Hello, world!',
status: 'published'
});
console.log(response.data); // Created entry with ID
```
### Update an Entry
```javascript
// Partial update (PATCH)
const response = await client.entries.patch('posts', 1, {
title: 'Updated Title'
});
// Full update (PUT)
const response = await client.entries.put('posts', 1, {
title: 'New Title',
content: 'New content',
status: 'draft'
});
```
### Delete an Entry
```javascript
await client.entries.delete('posts', 1);
```
## Using Criteria for Queries
```javascript
import { Criteria } from '@laravel-streams/api-client';
// Build a query
const criteria = new Criteria()
.where('status', 'published')
.where('views', '>', 100)
.orderBy('created_at', 'desc')
.limit(10);
// Execute the query
const response = await client.entries.get('posts', {
criteria: criteria
});
console.log(response.data); // Filtered and sorted entries
```
## Adding Authentication
```javascript
import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client';
// Create auth middleware
const auth = new AuthorizationMiddleware({
token: 'your-api-token',
type: 'Bearer'
});
// Add to client
const client = new Client({
baseURL: 'http://localhost/api',
middlewares: [auth]
});
// All requests will now include: Authorization: Bearer your-api-token
```
## Error Handling
```javascript
try {
const response = await client.entries.find('posts', 999);
console.log(response.data);
} catch (error) {
if (error.status === 404) {
console.log('Entry not found');
} else {
console.error('Error:', error.message);
}
}
```
## Complete Example
Here's a complete example putting it all together:
```javascript
import { Client, Criteria, AuthorizationMiddleware } from '@laravel-streams/api-client';
// Setup client with authentication
const auth = new AuthorizationMiddleware({
token: process.env.API_TOKEN,
type: 'Bearer'
});
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [auth]
});
// Fetch and display published posts
async function getPublishedPosts() {
try {
const criteria = new Criteria()
.where('status', 'published')
.orderBy('created_at', 'desc')
.limit(10);
const response = await client.entries.get('posts', { criteria });
response.data.forEach(post => {
console.log(`${post.title} - ${post.created_at}`);
});
} catch (error) {
console.error('Failed to fetch posts:', error.message);
}
}
// Create a new post
async function createPost(data) {
try {
const response = await client.entries.post('posts', data);
console.log('Post created with ID:', response.data.id);
return response.data;
} catch (error) {
console.error('Failed to create post:', error.message);
}
}
// Run examples
await getPublishedPosts();
await createPost({
title: 'Hello World',
content: 'This is my first post!',
status: 'draft'
});
```
## Next Steps
- [Client Configuration](/docs/client/client) - Learn about all configuration options
- [Criteria Query Builder](/docs/client/criteria) - Master the query builder
- [Middleware](/docs/client/middleware) - Extend functionality with middleware
- [Examples](/docs/client/examples) - See more real-world examples
---
# Client reference: Client Configuration
> Configure the API client for your application
Source: https://streams.dev/docs/client/client
The `Client` class is the main entry point for interacting with the Streams API.
## Basic Configuration
```javascript
import { Client } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'http://localhost/api'
});
```
## Configuration Options
### baseURL (required)
The base URL for your API endpoint:
```javascript
const client = new Client({
baseURL: 'https://api.example.com'
});
```
### request
Default request configuration applied to all requests:
```javascript
const client = new Client({
baseURL: 'http://localhost/api',
request: {
mode: 'cors',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include', // Send cookies
cache: 'no-cache'
}
});
```
### middlewares
Array of middleware to apply to requests:
```javascript
import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'http://localhost/api',
middlewares: [
new AuthorizationMiddleware({
token: 'api-token',
type: 'Bearer'
})
]
});
```
## Default Middlewares
The client includes these middlewares by default:
- `RequestDataMiddleware` - Handles request body serialization
- `CriteriaMiddleware` - Processes Criteria query parameters
- `QueryMiddleware` - Converts query parameters to query strings
- `ResponseDataMiddleware` - Parses response data
You can customize the default middlewares:
```javascript
const client = new Client({
baseURL: 'http://localhost/api',
defaultMiddlewares: [], // Disable default middlewares
middlewares: [
// Add your custom middlewares
]
});
```
## Adding Middleware Dynamically
You can add middleware after client creation:
```javascript
const client = new Client({
baseURL: 'http://localhost/api'
});
// Add authentication later
const auth = new AuthorizationMiddleware({
token: getUserToken()
});
client.use(auth);
```
## Environment-Based Configuration
```javascript
const config = {
development: {
baseURL: 'http://localhost:8000/api',
request: {
mode: 'cors',
credentials: 'include'
}
},
production: {
baseURL: 'https://api.production.com',
request: {
mode: 'cors',
credentials: 'same-origin'
}
}
};
const env = process.env.NODE_ENV || 'development';
const client = new Client(config[env]);
```
## Resources
The client automatically initializes resource instances:
### Streams Resource
```javascript
client.streams.get()
client.streams.find(stream)
client.streams.post(data)
client.streams.patch(stream, data)
client.streams.put(stream, data)
client.streams.delete(stream)
```
### Entries Resource
```javascript
client.entries.get(stream, config)
client.entries.find(stream, id, config)
client.entries.post(stream, data, config)
client.entries.patch(stream, id, data, config)
client.entries.put(stream, id, data, config)
client.entries.delete(stream, id, config)
```
## Request Method
For custom requests, use the `request` method directly:
```javascript
const response = await client.request('GET', '/custom/endpoint', {
query: { filter: 'active' },
headers: { 'X-Custom-Header': 'value' }
});
```
## Complete Example
```javascript
import {
Client,
AuthorizationMiddleware,
ETagMiddleware
} from '@laravel-streams/api-client';
// Custom logging middleware
class LoggingMiddleware {
constructor() {
this.options = {
priority: {
request: 10,
response: 90,
error: 50
}
};
}
async request(request, client) {
console.log(`→ ${request.method} ${request.url}`);
return request;
}
async response(response, client) {
console.log(`← ${response.status} ${response.statusText}`);
return response;
}
async error(error, client) {
console.error(`✗ ${error.message}`);
throw error;
}
}
// Create client with full configuration
const client = new Client({
baseURL: process.env.API_URL || 'http://localhost/api',
request: {
mode: 'cors',
credentials: 'include',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
},
middlewares: [
new LoggingMiddleware(),
new AuthorizationMiddleware({
token: process.env.API_TOKEN,
type: 'Bearer'
}),
new ETagMiddleware()
]
});
export default client;
```
## Next Steps
- [Working with Streams](/docs/client/streams)
- [Working with Entries](/docs/client/entries)
- [Middleware](/docs/client/middleware)
---
# Client reference: Streams
> Working with stream resources and CRUD operations
Source: https://streams.dev/docs/client/streams
Streams represent collections or resource types in the Laravel Streams API. The `Streams` class provides methods for managing stream resources.
## Getting Streams
### Get All Streams
```javascript
const response = await client.streams.get();
console.log(response.data); // Array of stream objects
```
### Get Specific Stream
```javascript
const response = await client.streams.find('posts');
console.log(response.data); // Single stream object
```
### With Criteria
```javascript
import { Criteria } from '@laravel-streams/api-client';
const criteria = new Criteria()
.where('type', 'content')
.orderBy('name', 'asc');
const response = await client.streams.get({ criteria });
```
## Creating Streams
### Basic Creation
```javascript
const stream = {
id: 'posts',
name: 'Posts',
description: 'Blog posts stream',
fields: {
title: {
type: 'string',
required: true
},
content: {
type: 'text',
required: true
},
author: {
type: 'relationship',
related: 'users'
},
published_at: {
type: 'datetime',
nullable: true
}
}
};
const response = await client.streams.post(stream);
```
### With Validation
```javascript
const stream = {
id: 'products',
name: 'Products',
rules: {
title: 'required|string|max:255',
price: 'required|numeric|min:0',
sku: 'required|unique:products,sku'
},
fields: {
title: { type: 'string' },
price: { type: 'decimal' },
sku: { type: 'string' }
}
};
const response = await client.streams.post(stream);
```
## Updating Streams
### Full Update (PUT)
```javascript
const updatedStream = {
id: 'posts',
name: 'Blog Posts',
description: 'Updated description',
fields: {
title: { type: 'string', required: true },
content: { type: 'text', required: true },
excerpt: { type: 'text' }, // New field
published_at: { type: 'datetime' }
}
};
const response = await client.streams.put('posts', updatedStream);
```
### Partial Update (PATCH)
```javascript
const updates = {
description: 'Updated description only',
fields: {
featured: { type: 'boolean', default: false }
}
};
const response = await client.streams.patch('posts', updates);
```
## Deleting Streams
```javascript
const response = await client.streams.delete('posts');
console.log(response.status); // 204 No Content
```
## Stream Structure
### Basic Stream Definition
```javascript
const stream = {
// Identity
id: 'posts', // Required: Unique identifier
name: 'Posts', // Required: Human-readable name
description: 'Blog posts', // Optional: Description
// Configuration
source: {
type: 'eloquent', // Storage type
model: 'App\\Models\\Post' // Model class
},
// Fields
fields: {
title: {
type: 'string',
required: true,
rules: 'max:255'
},
content: {
type: 'text',
required: true
}
},
// Validation
rules: {
title: 'required|string|max:255',
content: 'required|string'
}
};
```
### Field Types
```javascript
const stream = {
id: 'examples',
name: 'Examples',
fields: {
// Text fields
string_field: { type: 'string' },
text_field: { type: 'text' },
markdown_field: { type: 'markdown' },
// Number fields
integer_field: { type: 'integer' },
decimal_field: { type: 'decimal', decimals: 2 },
// Boolean
boolean_field: { type: 'boolean', default: false },
// Date/Time
date_field: { type: 'date' },
datetime_field: { type: 'datetime' },
timestamp_field: { type: 'timestamp' },
// Relationships
user: {
type: 'relationship',
related: 'users',
relationship_type: 'belongsTo'
},
// JSON
metadata: { type: 'object' },
tags: { type: 'array' },
// Files
image: { type: 'image' },
file: { type: 'file' }
}
};
```
## Real-World Examples
### Blog Stream
```javascript
const blogStream = {
id: 'posts',
name: 'Blog Posts',
description: 'Blog content management',
fields: {
title: {
type: 'string',
required: true,
rules: 'max:255'
},
slug: {
type: 'string',
required: true,
unique: true
},
excerpt: {
type: 'text',
rules: 'max:500'
},
content: {
type: 'markdown',
required: true
},
featured_image: {
type: 'image'
},
author: {
type: 'relationship',
related: 'users',
relationship_type: 'belongsTo'
},
category: {
type: 'relationship',
related: 'categories',
relationship_type: 'belongsTo'
},
tags: {
type: 'relationship',
related: 'tags',
relationship_type: 'belongsToMany'
},
status: {
type: 'string',
default: 'draft',
rules: 'in:draft,published,archived'
},
published_at: {
type: 'datetime',
nullable: true
}
},
rules: {
title: 'required|string|max:255',
slug: 'required|unique:posts,slug',
content: 'required',
status: 'required|in:draft,published,archived'
}
};
const response = await client.streams.post(blogStream);
```
### E-commerce Product Stream
```javascript
const productStream = {
id: 'products',
name: 'Products',
description: 'E-commerce product catalog',
fields: {
name: {
type: 'string',
required: true
},
sku: {
type: 'string',
required: true,
unique: true
},
description: {
type: 'text'
},
price: {
type: 'decimal',
decimals: 2,
required: true
},
sale_price: {
type: 'decimal',
decimals: 2,
nullable: true
},
cost: {
type: 'decimal',
decimals: 2
},
stock: {
type: 'integer',
default: 0
},
images: {
type: 'array',
items: {
type: 'image'
}
},
category: {
type: 'relationship',
related: 'categories'
},
attributes: {
type: 'object'
},
is_active: {
type: 'boolean',
default: true
},
weight: {
type: 'decimal',
decimals: 2
},
dimensions: {
type: 'object'
}
},
rules: {
name: 'required|string|max:255',
sku: 'required|unique:products,sku',
price: 'required|numeric|min:0',
stock: 'integer|min:0'
}
};
const response = await client.streams.post(productStream);
```
### User Management Stream
```javascript
const userStream = {
id: 'users',
name: 'Users',
description: 'User accounts and profiles',
fields: {
email: {
type: 'email',
required: true,
unique: true
},
name: {
type: 'string',
required: true
},
username: {
type: 'string',
required: true,
unique: true
},
password: {
type: 'password',
required: true
},
avatar: {
type: 'image',
nullable: true
},
bio: {
type: 'text',
nullable: true
},
role: {
type: 'relationship',
related: 'roles'
},
status: {
type: 'string',
default: 'active',
rules: 'in:active,inactive,banned'
},
email_verified_at: {
type: 'datetime',
nullable: true
},
last_login_at: {
type: 'datetime',
nullable: true
}
},
rules: {
email: 'required|email|unique:users,email',
name: 'required|string|max:255',
username: 'required|unique:users,username|alpha_dash',
password: 'required|min:8',
status: 'in:active,inactive,banned'
}
};
const response = await client.streams.post(userStream);
```
## Listing and Managing Streams
### Get All Streams with Details
```javascript
const response = await client.streams.get();
response.data.forEach(stream => {
console.log(`Stream: ${stream.name} (${stream.id})`);
console.log(`Fields: ${Object.keys(stream.fields).length}`);
console.log(`Description: ${stream.description}`);
});
```
### Filter Streams
```javascript
import { Criteria } from '@laravel-streams/api-client';
// Get only content-type streams
const criteria = new Criteria()
.where('source.type', 'eloquent')
.orderBy('name', 'asc');
const response = await client.streams.get({ criteria });
```
### Update Stream Configuration
```javascript
// Add new field to existing stream
const updates = {
fields: {
view_count: {
type: 'integer',
default: 0
}
}
};
await client.streams.patch('posts', updates);
```
## Response Structure
```javascript
// Success response
{
data: {
id: 'posts',
name: 'Posts',
description: 'Blog posts',
fields: { /* ... */ },
rules: { /* ... */ }
},
status: 200,
headers: { /* ... */ }
}
// List response
{
data: [
{ id: 'posts', name: 'Posts', /* ... */ },
{ id: 'users', name: 'Users', /* ... */ }
],
status: 200,
headers: { /* ... */ }
}
```
## Next Steps
- [Working with Entries](/docs/client/entries) - CRUD operations on stream entries
- [Criteria](/docs/client/criteria) - Query builder for filtering and sorting
- [Examples](/docs/client/examples) - Real-world usage patterns
---
# Client reference: Working with Entries
> CRUD operations for stream entries
Source: https://streams.dev/docs/client/entries
Entries represent the data within your configured Streams. Use the Entries API to interact with stream data.
## Get Entries
Return entries from a configured stream:
**Method:** `client.entries.get(stream, config)`
```javascript
const response = await client.entries.get('posts');
console.log(response.data); // Array of entries
```
### With Pagination
```javascript
const response = await client.entries.get('posts', {
query: {
per_page: 25,
page: 2
}
});
```
### With Criteria
```javascript
import { Criteria } from '@laravel-streams/api-client';
const criteria = new Criteria()
.where('status', 'published')
.orderBy('created_at', 'desc')
.limit(10);
const response = await client.entries.get('posts', { criteria });
```
### Response Structure
```javascript
{
"data": [
{
"id": "1",
"title": "Hello World",
"status": "published"
}
],
"meta": {
"total": 100,
"per_page": 25,
"current_page": 1,
"last_page": 4
},
"links": {
"self": "http://api.example.com/streams/posts/entries",
"first": "http://api.example.com/streams/posts/entries?page=1",
"next": "http://api.example.com/streams/posts/entries?page=2"
}
}
```
## Find Entry
Return a single entry by ID:
**Method:** `client.entries.find(stream, id, config)`
```javascript
const response = await client.entries.find('posts', 1);
console.log(response.data); // Single entry object
```
### Response Structure
```javascript
{
"data": {
"id": "1",
"title": "Hello World",
"content": "This is my first post",
"status": "published",
"created_at": "2024-01-01T00:00:00.000Z"
},
"links": {
"self": "http://api.example.com/streams/posts/entries/1",
"stream": "http://api.example.com/streams/posts"
}
}
```
## Create Entry
Create a new entry in a stream:
**Method:** `client.entries.post(stream, data, config)`
```javascript
const response = await client.entries.post('posts', {
title: 'My New Post',
content: 'This is the content',
status: 'draft'
});
console.log(response.data.id); // ID of created entry
```
### Response Structure
```javascript
{
"data": {
"id": "123",
"title": "My New Post",
"content": "This is the content",
"status": "draft",
"created_at": "2024-01-01T12:00:00.000Z"
},
"links": {
"self": "http://api.example.com/streams/posts/entries/123",
"location": "http://api.example.com/streams/posts/entries/123"
}
}
```
## Update Entry (Partial)
Update specific fields of an entry:
**Method:** `client.entries.patch(stream, id, data, config)`
```javascript
// Only update the title
const response = await client.entries.patch('posts', 1, {
title: 'Updated Title'
});
// Other fields remain unchanged
console.log(response.data);
```
## Update Entry (Full)
Replace all fields of an entry:
**Method:** `client.entries.put(stream, id, data, config)`
```javascript
// Replace entire entry
const response = await client.entries.put('posts', 1, {
title: 'New Title',
content: 'New content',
status: 'published'
});
// Omitted fields will be set to null/default
```
### PATCH vs PUT
- **PATCH**: Updates only the provided fields, keeps others unchanged
- **PUT**: Replaces the entire entry, omitted fields become null
```javascript
// Original entry
{ id: 1, title: 'Hello', content: 'World', status: 'draft' }
// PATCH { title: 'Hi' }
{ id: 1, title: 'Hi', content: 'World', status: 'draft' }
// PUT { title: 'Hi' }
{ id: 1, title: 'Hi', content: null, status: null }
```
## Delete Entry
Delete an entry from a stream:
**Method:** `client.entries.delete(stream, id, config)`
```javascript
await client.entries.delete('posts', 1);
// Returns empty response on success
```
## Filtering Results
Use Criteria to filter entries:
```javascript
import { Criteria } from '@laravel-streams/api-client';
const criteria = new Criteria()
.where('status', 'published')
.where('created_at', '>=', '2024-01-01')
.where('views', '>', 100);
const response = await client.entries.get('posts', { criteria });
```
### Available Operators
```javascript
// Comparison
.where('views', '>', 100)
.where('views', '>=', 100)
.where('views', '<', 100)
.where('views', '<=', 100)
.where('status', '==', 'published')
.where('status', '!=', 'draft')
// Logical
.where('tags', 'IN', ['tech', 'news'])
.where('content', 'LIKE', '%tutorial%')
```
## Sorting Results
```javascript
const criteria = new Criteria()
.orderBy('created_at', 'desc')
.orderBy('title', 'asc');
const response = await client.entries.get('posts', { criteria });
```
## Pagination
```javascript
// Simple pagination
const criteria = new Criteria()
.paginate(25, 2); // 25 per page, page 2
const response = await client.entries.get('posts', { criteria });
console.log(response.meta.total);
console.log(response.meta.current_page);
console.log(response.meta.last_page);
```
## Error Handling
```javascript
try {
const response = await client.entries.find('posts', 999);
} catch (error) {
if (error.status === 404) {
console.log('Entry not found');
} else if (error.status === 422) {
console.log('Validation error:', error.response.data.errors);
} else {
console.error('Error:', error.message);
}
}
```
## Real-World Examples
### Blog Post Management
```javascript
// Get recent published posts
async function getRecentPosts(limit = 10) {
const criteria = new Criteria()
.where('status', 'published')
.orderBy('published_at', 'desc')
.limit(limit);
return await client.entries.get('posts', { criteria });
}
// Publish a draft
async function publishPost(id) {
return await client.entries.patch('posts', id, {
status: 'published',
published_at: new Date().toISOString()
});
}
// Search posts
async function searchPosts(query) {
const criteria = new Criteria()
.where('title', 'LIKE', `%${query}%`)
.orWhere('content', 'LIKE', `%${query}%`)
.where('status', 'published')
.orderBy('relevance', 'desc');
return await client.entries.get('posts', { criteria });
}
```
### User Management
```javascript
// Get active users
async function getActiveUsers() {
const criteria = new Criteria()
.where('status', 'active')
.where('last_login', '>=', getLastWeek())
.orderBy('last_login', 'desc');
return await client.entries.get('users', { criteria });
}
// Update user profile
async function updateProfile(userId, data) {
return await client.entries.patch('users', userId, {
name: data.name,
email: data.email,
bio: data.bio,
updated_at: new Date().toISOString()
});
}
```
## Next Steps
- [Criteria Query Builder](/docs/client/criteria) - Learn advanced querying
- [Middleware](/docs/client/middleware) - Add custom functionality
- [Examples](/docs/client/examples) - See more examples
---
# Client reference: Criteria Query Builder
> PHP Laravel-style query building for filtering and sorting
Source: https://streams.dev/docs/client/criteria
The Criteria class provides a fluent, PHP Laravel-style interface for building queries.
## Basic Usage
```javascript
import { Criteria } from '@laravel-streams/api-client';
const criteria = new Criteria()
.where('status', 'published')
.orderBy('created_at', 'desc')
.limit(10);
const response = await client.entries.get('posts', { criteria });
```
## Creating Criteria
### Static Factory
```javascript
const criteria = Criteria.make()
.where('status', 'active');
```
### Constructor
```javascript
const criteria = new Criteria();
criteria.where('status', 'active');
```
## Where Clauses
### Basic Where
```javascript
// Simple equality
criteria.where('status', 'published');
// Generates: WHERE status == 'published'
// With operator
criteria.where('views', '>', 100);
// Generates: WHERE views > 100
```
### Available Operators
#### Comparison Operators
```javascript
criteria.where('views', '>', 100); // Greater than
criteria.where('views', '>=', 100); // Greater than or equal
criteria.where('views', '<', 100); // Less than
criteria.where('views', '<=', 100); // Less than or equal
criteria.where('status', '==', 'active'); // Equal
criteria.where('status', '!=', 'deleted'); // Not equal
criteria.where('value', '<>', 0); // Not equal (alternative)
```
#### Logical Operators
```javascript
criteria.where('status', 'IN', ['published', 'featured']);
criteria.where('title', 'LIKE', '%tutorial%');
criteria.where('content', 'NOT', null);
criteria.where('tags', 'ALL', ['javascript', 'tutorial']);
criteria.where('categories', 'ANY', ['tech', 'news']);
```
### OR Where Clauses
```javascript
const criteria = new Criteria()
.where('status', 'published')
.orWhere('status', 'featured');
// WHERE status == 'published' OR status == 'featured'
```
### Chaining Where Clauses
```javascript
const criteria = new Criteria()
.where('status', 'published')
.where('views', '>', 100)
.where('category', 'technology')
.where('created_at', '>=', '2024-01-01');
// WHERE status == 'published'
// AND views > 100
// AND category == 'technology'
// AND created_at >= '2024-01-01'
```
## Ordering Results
### Order By
```javascript
// Descending (default)
criteria.orderBy('created_at');
// Ascending
criteria.orderBy('title', 'asc');
// Descending
criteria.orderBy('views', 'desc');
```
### Multiple Order By
```javascript
const criteria = new Criteria()
.orderBy('priority', 'desc') // First by priority
.orderBy('created_at', 'desc') // Then by date
.orderBy('title', 'asc'); // Then by title
```
## Limiting Results
### Limit
```javascript
criteria.limit(10); // Return maximum 10 results
```
### First
```javascript
criteria.first(); // Same as limit(1)
```
### Find by ID
```javascript
criteria.find(123);
// Same as: where('id', 123).limit(1)
```
## Pagination
```javascript
// paginate(perPage, page)
criteria.paginate(25, 1); // 25 per page, page 1
// Default pagination (100 per page, page 1)
criteria.paginate();
```
## Complex Queries
### Combining Conditions
```javascript
const criteria = new Criteria()
// Published or featured
.where('status', 'published')
.orWhere('status', 'featured')
// With minimum views
.where('views', '>=', 100)
// In specific categories
.where('category', 'IN', ['tech', 'tutorial'])
// Recent posts
.where('created_at', '>=', '2024-01-01')
// Sort by popularity, then date
.orderBy('views', 'desc')
.orderBy('created_at', 'desc')
// Paginate
.paginate(20, 1);
```
### Search Functionality
```javascript
function searchPosts(query) {
return new Criteria()
.where('title', 'LIKE', `%${query}%`)
.orWhere('content', 'LIKE', `%${query}%`)
.orWhere('tags', 'LIKE', `%${query}%`)
.where('status', 'published')
.orderBy('relevance', 'desc')
.limit(50);
}
const criteria = searchPosts('javascript');
const results = await client.entries.get('posts', { criteria });
```
## Working with Parameters
### Get Parameters
```javascript
const criteria = new Criteria()
.where('status', 'published')
.orderBy('created_at', 'desc')
.limit(10);
const params = criteria.getParameters();
console.log(params);
// [
// { name: 'where', value: ['status', '==', 'published', null] },
// { name: 'orderBy', value: ['created_at', 'desc'] },
// { name: 'limit', value: 10 }
// ]
```
### Set Parameters
```javascript
const criteria = new Criteria();
criteria.setParameters([
{ name: 'where', value: ['status', '==', 'published', null] },
{ name: 'limit', value: 10 }
]);
```
### Add Parameter
```javascript
const criteria = new Criteria();
criteria.addParameter('where', ['status', '==', 'published', null]);
```
### Standardize Parameters
```javascript
const criteria = new Criteria()
.where('status', 'published')
.limit(10);
const standardized = criteria.standardizeParameters();
console.log(standardized);
// {
// where: ['status', '==', 'published', null],
// limit: 10
// }
```
## Real-World Examples
### Blog Post Filtering
```javascript
// Featured posts from last month
const criteria = new Criteria()
.where('featured', true)
.where('published_at', '>=', getLastMonth())
.where('status', 'published')
.orderBy('published_at', 'desc')
.limit(5);
// Posts by category with minimum engagement
const criteria = new Criteria()
.where('category', 'javascript')
.where('likes', '>', 50)
.where('comments', '>', 10)
.orderBy('engagement_score', 'desc')
.paginate(20, 1);
```
### E-commerce Product Filtering
```javascript
// Products in price range, in stock
const criteria = new Criteria()
.where('price', '>=', 10)
.where('price', '<=', 100)
.where('stock', '>', 0)
.where('category', 'electronics')
.orderBy('popularity', 'desc')
.paginate(24, 1);
// Sale items
const criteria = new Criteria()
.where('on_sale', true)
.where('discount', '>=', 20)
.where('stock', '>', 0)
.orderBy('discount', 'desc')
.orderBy('price', 'asc');
```
### User Management
```javascript
// Active users who logged in recently
const criteria = new Criteria()
.where('status', 'active')
.where('last_login', '>=', getLastWeek())
.where('email_verified', true)
.orderBy('last_login', 'desc')
.limit(100);
// Premium users expiring soon
const criteria = new Criteria()
.where('subscription', 'premium')
.where('expires_at', '<=', getNextMonth())
.where('expires_at', '>=', getToday())
.orderBy('expires_at', 'asc');
```
## Builder Pattern
The Criteria class uses the builder pattern, allowing you to chain methods:
```javascript
const criteria = new Criteria()
.where('a', 1) // Returns this
.where('b', 2) // Returns this
.orderBy('c') // Returns this
.limit(10); // Returns this
// Same as:
const criteria = new Criteria();
criteria.where('a', 1);
criteria.where('b', 2);
criteria.orderBy('c');
criteria.limit(10);
```
## Reusable Criteria
```javascript
// Create base criteria
const publishedPosts = () => new Criteria()
.where('status', 'published')
.where('deleted_at', null);
// Extend it
const recentPublishedPosts = publishedPosts()
.where('created_at', '>=', getLastWeek())
.orderBy('created_at', 'desc');
const popularPublishedPosts = publishedPosts()
.where('views', '>', 1000)
.orderBy('views', 'desc');
```
## Next Steps
- [Working with Entries](/docs/client/entries) - Apply criteria to entry queries
- [Examples](/docs/client/examples) - See more complex examples
---
# Client reference: Middleware System
> Request/response middleware and custom middleware creation
Source: https://streams.dev/docs/client/middleware
The middleware system allows you to intercept and modify requests and responses. Middleware can transform data, add authentication, handle errors, and more.
## Built-in Middleware
### AuthorizationMiddleware
Adds Bearer token authentication to requests.
```javascript
import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new AuthorizationMiddleware({ token: 'your-api-token' })
]
});
```
**What it does:**
- Adds `Authorization: Bearer {token}` header to all requests
- Enables authenticated API access
### CriteriaMiddleware
Converts Criteria objects to query parameters.
```javascript
import { Client, CriteriaMiddleware, Criteria } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new CriteriaMiddleware()
]
});
// Criteria is automatically converted to query params
const criteria = new Criteria()
.where('status', 'published')
.limit(10);
const response = await client.entries.get('posts', { criteria });
```
**What it does:**
- Transforms `Criteria` objects into URL query parameters
- Enables fluent query building with Laravel-style syntax
### QueryMiddleware
Adds query parameters to requests.
```javascript
import { Client, QueryMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new QueryMiddleware()
]
});
// Add query params via options
const response = await client.get('/posts', {
query: {
status: 'published',
limit: 10
}
});
// GET /posts?status=published&limit=10
```
**What it does:**
- Converts `query` option to URL query string
- Handles URL encoding automatically
### RequestDataMiddleware
Transforms request data based on content type.
```javascript
import { Client, RequestDataMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new RequestDataMiddleware()
]
});
// JSON (default)
await client.post('/posts', {
data: { title: 'Hello', content: 'World' }
});
// Content-Type: application/json
// Body: {"title":"Hello","content":"World"}
// Form data
await client.post('/upload', {
data: { file: fileBlob },
headers: { 'Content-Type': 'multipart/form-data' }
});
// Content-Type: multipart/form-data
// Body: FormData object
```
**What it does:**
- JSON: Stringifies data and sets `Content-Type: application/json`
- Form Data: Converts to FormData and sets `Content-Type: multipart/form-data`
- URL Encoded: Converts to URLSearchParams and sets `Content-Type: application/x-www-form-urlencoded`
### ResponseDataMiddleware
Parses response data based on content type.
```javascript
import { Client, ResponseDataMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ResponseDataMiddleware()
]
});
const response = await client.get('/posts');
// Automatically parses JSON response
console.log(response.data); // Parsed object
```
**What it does:**
- Parses JSON responses automatically
- Handles text responses
- Returns raw response for other content types
### ETagMiddleware
Implements HTTP caching with ETags.
```javascript
import { Client, ETagMiddleware } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ETagMiddleware()
]
});
// First request - stores ETag
const response1 = await client.get('/posts/1');
// Response includes: ETag: "abc123"
// Second request - sends If-None-Match
const response2 = await client.get('/posts/1');
// Request includes: If-None-Match: "abc123"
// If not modified: 304 status, returns cached data
```
**What it does:**
- Stores ETags from responses
- Adds `If-None-Match` header on subsequent requests
- Returns cached data for 304 (Not Modified) responses
- Reduces bandwidth and improves performance
## Creating Custom Middleware
### Basic Middleware
```javascript
import { Middleware } from '@laravel-streams/api-client';
class LoggingMiddleware extends Middleware {
async handle(request, next) {
console.log('Request:', request.url);
const response = await next(request);
console.log('Response:', response.status);
return response;
}
}
// Use it
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new LoggingMiddleware()
]
});
```
### Request Transformation
```javascript
class ApiVersionMiddleware extends Middleware {
constructor(version = 'v1') {
super();
this.version = version;
}
async handle(request, next) {
// Add version to URL
request.url = `/api/${this.version}${request.url}`;
// Add version header
request.headers.set('X-API-Version', this.version);
return next(request);
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ApiVersionMiddleware('v2')
]
});
await client.get('/posts');
// GET https://api.example.com/api/v2/posts
// Headers: X-API-Version: v2
```
### Response Transformation
```javascript
class DataWrapperMiddleware extends Middleware {
async handle(request, next) {
const response = await next(request);
// Unwrap nested data structure
if (response.data && response.data.data) {
response.data = response.data.data;
}
return response;
}
}
```
### Error Handling
```javascript
class RetryMiddleware extends Middleware {
constructor(maxRetries = 3) {
super();
this.maxRetries = maxRetries;
}
async handle(request, next) {
let lastError;
for (let i = 0; i < this.maxRetries; i++) {
try {
return await next(request);
} catch (error) {
lastError = error;
// Only retry on network errors or 5xx status
if (error.status < 500) {
throw error;
}
// Wait before retry
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
throw lastError;
}
}
```
### Request/Response Logging
```javascript
class DetailedLoggingMiddleware extends Middleware {
async handle(request, next) {
const startTime = Date.now();
console.log('→ Request:', {
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
body: request.body
});
try {
const response = await next(request);
const duration = Date.now() - startTime;
console.log('← Response:', {
status: response.status,
statusText: response.statusText,
duration: `${duration}ms`,
headers: Object.fromEntries(response.headers.entries()),
data: response.data
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.error('✗ Error:', {
duration: `${duration}ms`,
message: error.message,
status: error.status
});
throw error;
}
}
}
```
## Real-World Examples
### API Key Authentication
```javascript
class ApiKeyMiddleware extends Middleware {
constructor(apiKey) {
super();
this.apiKey = apiKey;
}
async handle(request, next) {
request.headers.set('X-API-Key', this.apiKey);
return next(request);
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ApiKeyMiddleware('your-api-key-here')
]
});
```
### Request Throttling
```javascript
class ThrottleMiddleware extends Middleware {
constructor(requestsPerSecond = 10) {
super();
this.delay = 1000 / requestsPerSecond;
this.lastRequest = 0;
}
async handle(request, next) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequest;
if (timeSinceLastRequest < this.delay) {
await new Promise(resolve =>
setTimeout(resolve, this.delay - timeSinceLastRequest)
);
}
this.lastRequest = Date.now();
return next(request);
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new ThrottleMiddleware(5) // Max 5 requests per second
]
});
```
### Response Caching
```javascript
class CacheMiddleware extends Middleware {
constructor(ttl = 60000) { // 60 seconds default
super();
this.cache = new Map();
this.ttl = ttl;
}
getCacheKey(request) {
return `${request.method}:${request.url}`;
}
async handle(request, next) {
// Only cache GET requests
if (request.method !== 'GET') {
return next(request);
}
const cacheKey = this.getCacheKey(request);
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log('Cache hit:', cacheKey);
return cached.response;
}
const response = await next(request);
this.cache.set(cacheKey, {
response: response,
timestamp: Date.now()
});
return response;
}
}
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new CacheMiddleware(30000) // Cache for 30 seconds
]
});
```
### Request/Response Transformation
```javascript
class TimestampMiddleware extends Middleware {
async handle(request, next) {
// Add timestamp to requests
if (request.body && typeof request.body === 'object') {
request.body.timestamp = Date.now();
}
const response = await next(request);
// Parse date strings in response
if (response.data) {
this.parseDates(response.data);
}
return response;
}
parseDates(obj) {
const dateFields = ['created_at', 'updated_at', 'published_at'];
for (const key in obj) {
if (dateFields.includes(key) && typeof obj[key] === 'string') {
obj[key] = new Date(obj[key]);
} else if (typeof obj[key] === 'object') {
this.parseDates(obj[key]);
}
}
}
}
```
## Middleware Execution Order
Middleware executes in the order it's added:
```javascript
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new LoggingMiddleware(), // 1. Logs request
new AuthMiddleware(), // 2. Adds auth
new CriteriaMiddleware(), // 3. Converts criteria
new RequestDataMiddleware(), // 4. Transforms request data
// --- Request sent ---
// --- Response received ---
new ResponseDataMiddleware(), // 5. Parses response data
new DataWrapperMiddleware() // 6. Unwraps data
]
});
```
The execution flow:
1. Request goes through middleware from top to bottom
2. Response comes back through middleware from bottom to top
3. Each middleware can modify request before calling `next()`
4. Each middleware can modify response after calling `next()`
## Best Practices
### Keep Middleware Focused
```javascript
// Good: Single responsibility
class AuthMiddleware extends Middleware {
async handle(request, next) {
request.headers.set('Authorization', `Bearer ${this.token}`);
return next(request);
}
}
// Bad: Too many responsibilities
class MegaMiddleware extends Middleware {
async handle(request, next) {
// Adds auth, transforms data, logs, caches, retries...
// Too much in one middleware!
}
}
```
### Error Handling
```javascript
class SafeMiddleware extends Middleware {
async handle(request, next) {
try {
// Your middleware logic
return await next(request);
} catch (error) {
// Handle or re-throw
console.error('Middleware error:', error);
throw error;
}
}
}
```
### Configuration
```javascript
class ConfigurableMiddleware extends Middleware {
constructor(options = {}) {
super();
this.options = {
enabled: true,
timeout: 5000,
...options
};
}
async handle(request, next) {
if (!this.options.enabled) {
return next(request);
}
// Use this.options...
return next(request);
}
}
```
## Next Steps
- [Examples](/docs/client/examples) - See middleware in real-world scenarios
- [Client Configuration](/docs/client/client) - Learn more about client setup
---
# Client reference: Examples
> Real-world usage patterns and complete examples
Source: https://streams.dev/docs/client/examples
Complete examples demonstrating real-world usage patterns.
## Basic Blog Application
### Setup
```javascript
import { Client, AuthorizationMiddleware, CriteriaMiddleware, Criteria } from '@laravel-streams/api-client';
const client = new Client({
baseURL: 'https://api.example.com',
middlewares: [
new AuthorizationMiddleware({ token: 'your-api-token' }),
new CriteriaMiddleware()
]
});
```
### List Blog Posts
```javascript
async function getBlogPosts(page = 1, perPage = 10) {
const criteria = new Criteria()
.where('status', 'published')
.orderBy('published_at', 'desc')
.paginate(perPage, page);
const response = await client.entries.get('posts', { criteria });
return {
posts: response.data,
page: page,
perPage: perPage
};
}
// Usage
const { posts } = await getBlogPosts(1, 10);
posts.forEach(post => {
console.log(`${post.title} - ${post.published_at}`);
});
```
### Get Single Post
```javascript
async function getPost(id) {
const response = await client.entries.find('posts', id);
return response.data;
}
// Usage
const post = await getPost(123);
console.log(post.title);
console.log(post.content);
```
### Create Post
```javascript
async function createPost(data) {
const post = {
title: data.title,
slug: data.slug || slugify(data.title),
content: data.content,
excerpt: data.excerpt,
author_id: data.authorId,
category_id: data.categoryId,
status: 'draft',
published_at: null
};
const response = await client.entries.post('posts', post);
return response.data;
}
// Usage
const newPost = await createPost({
title: 'Getting Started with Laravel Streams',
content: 'This is a comprehensive guide...',
excerpt: 'Learn the basics of Laravel Streams',
authorId: 1,
categoryId: 5
});
```
### Update Post
```javascript
async function updatePost(id, updates) {
const response = await client.entries.patch('posts', id, updates);
return response.data;
}
// Usage
await updatePost(123, {
title: 'Updated Title',
content: 'Updated content...'
});
```
### Publish Post
```javascript
async function publishPost(id) {
const updates = {
status: 'published',
published_at: new Date().toISOString()
};
return await updatePost(id, updates);
}
// Usage
await publishPost(123);
```
### Delete Post
```javascript
async function deletePost(id) {
await client.entries.delete('posts', id);
}
// Usage
await deletePost(123);
```
### Search Posts
```javascript
async function searchPosts(query, filters = {}) {
const criteria = new Criteria()
// Search in title and content
.where('title', 'LIKE', `%${query}%`)
.orWhere('content', 'LIKE', `%${query}%`)
// Always published
.where('status', 'published');
// Optional filters
if (filters.category) {
criteria.where('category_id', filters.category);
}
if (filters.author) {
criteria.where('author_id', filters.author);
}
if (filters.tag) {
criteria.where('tags', 'LIKE', `%${filters.tag}%`);
}
criteria.orderBy('published_at', 'desc').limit(20);
const response = await client.entries.get('posts', { criteria });
return response.data;
}
// Usage
const results = await searchPosts('javascript', {
category: 5,
tag: 'tutorial'
});
```
### Posts by Category
```javascript
async function getPostsByCategory(categoryId, page = 1) {
const criteria = new Criteria()
.where('category_id', categoryId)
.where('status', 'published')
.orderBy('published_at', 'desc')
.paginate(10, page);
const response = await client.entries.get('posts', { criteria });
return response.data;
}
// Usage
const techPosts = await getPostsByCategory(5);
```
## E-commerce Application
### Product Catalog
```javascript
async function getProducts(filters = {}) {
const criteria = new Criteria()
.where('is_active', true);
// Price range
if (filters.minPrice) {
criteria.where('price', '>=', filters.minPrice);
}
if (filters.maxPrice) {
criteria.where('price', '<=', filters.maxPrice);
}
// Category
if (filters.category) {
criteria.where('category_id', filters.category);
}
// In stock only
if (filters.inStock) {
criteria.where('stock', '>', 0);
}
// On sale
if (filters.onSale) {
criteria.where('sale_price', '!=', null);
}
// Sorting
const sortBy = filters.sortBy || 'created_at';
const sortOrder = filters.sortOrder || 'desc';
criteria.orderBy(sortBy, sortOrder);
criteria.paginate(24, filters.page || 1);
const response = await client.entries.get('products', { criteria });
return response.data;
}
// Usage
const products = await getProducts({
category: 10,
minPrice: 10,
maxPrice: 100,
inStock: true,
sortBy: 'price',
sortOrder: 'asc',
page: 1
});
```
### Shopping Cart
```javascript
class ShoppingCart {
constructor(client) {
this.client = client;
}
async addItem(productId, quantity = 1) {
const item = {
product_id: productId,
quantity: quantity
};
const response = await this.client.entries.post('cart_items', item);
return response.data;
}
async updateQuantity(itemId, quantity) {
const response = await this.client.entries.patch('cart_items', itemId, {
quantity: quantity
});
return response.data;
}
async removeItem(itemId) {
await this.client.entries.delete('cart_items', itemId);
}
async getItems() {
const criteria = new Criteria()
.where('user_id', this.getCurrentUserId())
.orderBy('created_at', 'desc');
const response = await this.client.entries.get('cart_items', { criteria });
return response.data;
}
async clear() {
const items = await this.getItems();
for (const item of items) {
await this.removeItem(item.id);
}
}
getCurrentUserId() {
// Get from your auth system
return 1;
}
}
// Usage
const cart = new ShoppingCart(client);
await cart.addItem(123, 2);
await cart.updateQuantity(456, 3);
const items = await cart.getItems();
```
### Order Management
```javascript
async function createOrder(cartItems) {
// Calculate totals
const subtotal = cartItems.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const tax = subtotal * 0.1; // 10% tax
const total = subtotal + tax;
// Create order
const order = {
subtotal: subtotal,
tax: tax,
total: total,
status: 'pending',
items: cartItems.map(item => ({
product_id: item.product_id,
quantity: item.quantity,
price: item.price
}))
};
const response = await client.entries.post('orders', order);
return response.data;
}
async function getOrderHistory(userId, page = 1) {
const criteria = new Criteria()
.where('user_id', userId)
.orderBy('created_at', 'desc')
.paginate(10, page);
const response = await client.entries.get('orders', { criteria });
return response.data;
}
// Usage
const order = await createOrder(cartItems);
const orders = await getOrderHistory(1);
```
## User Management
### Authentication
```javascript
class AuthService {
constructor(client) {
this.client = client;
this.token = localStorage.getItem('auth_token');
}
async login(email, password) {
const response = await this.client.post('/auth/login', {
data: { email, password }
});
this.token = response.data.token;
localStorage.setItem('auth_token', this.token);
// Update client with new token
this.client.middleware.forEach(middleware => {
if (middleware instanceof AuthorizationMiddleware) {
middleware.token = this.token;
}
});
return response.data.user;
}
async logout() {
await this.client.post('/auth/logout');
this.token = null;
localStorage.removeItem('auth_token');
}
async register(userData) {
const response = await this.client.post('/auth/register', {
data: userData
});
return response.data;
}
async getCurrentUser() {
const response = await this.client.get('/auth/me');
return response.data;
}
}
// Usage
const auth = new AuthService(client);
const user = await auth.login('user@example.com', 'password');
```
### User Profiles
```javascript
async function getUserProfile(userId) {
const response = await client.entries.find('users', userId);
return response.data;
}
async function updateProfile(userId, updates) {
const response = await client.entries.patch('users', userId, updates);
return response.data;
}
async function uploadAvatar(userId, file) {
const formData = new FormData();
formData.append('avatar', file);
const response = await client.post(`/users/${userId}/avatar`, {
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data.avatar_url;
}
// Usage
const profile = await getUserProfile(1);
await updateProfile(1, { bio: 'Updated bio' });
await uploadAvatar(1, avatarFile);
```
## Content Management
### Media Library
```javascript
class MediaLibrary {
constructor(client) {
this.client = client;
}
async upload(file, metadata = {}) {
const formData = new FormData();
formData.append('file', file);
formData.append('metadata', JSON.stringify(metadata));
const response = await this.client.post('/media/upload', {
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
}
async getFiles(filters = {}) {
const criteria = new Criteria();
if (filters.type) {
criteria.where('mime_type', 'LIKE', `${filters.type}/%`);
}
if (filters.search) {
criteria.where('filename', 'LIKE', `%${filters.search}%`);
}
criteria.orderBy('created_at', 'desc')
.paginate(filters.perPage || 20, filters.page || 1);
const response = await this.client.entries.get('media', { criteria });
return response.data;
}
async delete(fileId) {
await this.client.entries.delete('media', fileId);
}
}
// Usage
const media = new MediaLibrary(client);
const file = await media.upload(imageFile, { alt: 'Product image' });
const images = await media.getFiles({ type: 'image' });
await media.delete(123);
```
### Multi-language Content
```javascript
async function getTranslations(streamId, entryId) {
const criteria = new Criteria()
.where('translatable_type', streamId)
.where('translatable_id', entryId);
const response = await client.entries.get('translations', { criteria });
return response.data;
}
async function setTranslation(streamId, entryId, locale, field, value) {
const translation = {
translatable_type: streamId,
translatable_id: entryId,
locale: locale,
field: field,
value: value
};
const response = await client.entries.post('translations', translation);
return response.data;
}
// Usage
await setTranslation('posts', 123, 'es', 'title', 'Título en Español');
await setTranslation('posts', 123, 'es', 'content', 'Contenido en Español');
const translations = await getTranslations('posts', 123);
```
## Analytics and Reporting
### View Tracking
```javascript
async function trackView(streamId, entryId) {
await client.post('/analytics/view', {
data: {
stream: streamId,
entry: entryId,
timestamp: Date.now(),
user_agent: navigator.userAgent
}
});
}
async function getPopularPosts(days = 7) {
const since = new Date();
since.setDate(since.getDate() - days);
const criteria = new Criteria()
.where('status', 'published')
.where('published_at', '>=', since.toISOString())
.orderBy('views', 'desc')
.limit(10);
const response = await client.entries.get('posts', { criteria });
return response.data;
}
// Usage
await trackView('posts', 123);
const popular = await getPopularPosts(7);
```
## Error Handling
### Comprehensive Error Handling
```javascript
async function safeApiCall(operation) {
try {
return await operation();
} catch (error) {
if (error.status === 401) {
// Unauthorized - redirect to login
console.error('Authentication required');
window.location.href = '/login';
} else if (error.status === 403) {
// Forbidden
console.error('Access denied');
throw new Error('You do not have permission to perform this action');
} else if (error.status === 404) {
// Not found
console.error('Resource not found');
throw new Error('The requested resource was not found');
} else if (error.status === 422) {
// Validation error
console.error('Validation failed:', error.data);
throw new Error('Please check your input and try again');
} else if (error.status >= 500) {
// Server error
console.error('Server error:', error);
throw new Error('A server error occurred. Please try again later.');
} else {
// Other errors
console.error('API error:', error);
throw error;
}
}
}
// Usage
const post = await safeApiCall(() => getPost(123));
```
## Next Steps
- [Client Configuration](/docs/client/client) - Advanced client setup
- [Criteria](/docs/client/criteria) - Query building techniques
- [Middleware](/docs/client/middleware) - Custom middleware patterns
---
# Contributing: This project
> What streams.dev is and how it uses Streams Core and UI.
Source: https://streams.dev/docs/this-project
## What this repository is
**streams.dev** is the public documentation site and reference implementation for the Streams ecosystem. It is a Laravel 12 application (PHP 8.2 or newer) that ships with **streams/core** and **streams/ui** in production, plus **streams/sdk** as a dev dependency.
Unlike the generic `composer create-project streams/streams` starter, this repo is purpose-built to:
- Serve documentation from flat files in `streams/data/`
- Demonstrate stream-driven pages (`/`, `/docs`, `/addons`) without custom controllers
- Link to package source via Composer path repositories for local development
## What is in git
| Path | Role |
|------|------|
| `streams/*.json` | Stream definitions (pages, docs, packages, categories) |
| `streams/data/` | Filebase content: markdown docs, HTML pages, package catalog data |
| `resources/views/` | Blade layouts and partials used by pages and docs |
| `app/Providers/AppServiceProvider.php` | Registers the admin panel via `UI::panel()` |
| `composer.json` | Requires Core + UI; path repos for sibling package clones |
There is minimal application code. Most behavior comes from stream configuration and package service providers.
## Packages in this project
Production dependencies:
- [streams/core](/docs/core/introduction) — data modeling, repositories, routing
- [streams/ui](/docs/ui/introduction) — Livewire admin panel at `/admin`
Dev-only:
- [streams/sdk](/docs/sdk/introduction) — scaffolding helpers
**streams/api** is available as a path repository for local work but is **not** required in this project's `composer.json`. Add it when you need REST endpoints.
## Related
- [Project structure](/docs/project-structure) — directory map
- [Local development](/docs/local-development) — run the site locally
- [Installation](/docs/installation) — add Streams to your own Laravel app
---
# Contributing: Project structure
> Where stream definitions, content, views, and app code live.
Source: https://streams.dev/docs/project-structure
## Top-level layout
```text
streams.dev/
├── app/ # Minimal Laravel app code
├── streams/ # Stream JSON definitions
├── streams/data/ # Filebase content for streams
├── resources/views/ # Blade templates
├── routes/ # Laravel routes (mostly defaults)
├── config/ # Laravel + streams config
└── composer.json # Core, UI, path repos
```
## `streams/` — configuration
Each `.json` file defines one stream. Examples in this repo:
| File | Stream handle | Purpose |
|------|---------------|---------|
| `pages.json` | `pages` | Site pages at `/`, `/docs`, `/addons` |
| `docs.json` | `docs` | Hub documentation at `/docs/{id}` |
| `docs_categories.json` | `docs_categories` | Sidebar and index groupings |
| `packages.json` | `packages` | Add-on catalog (`type: self`) |
| `core_docs.json` | `core_docs` | Core reference at `/docs/core/{id}` |
| `ui_docs.json` | `ui_docs` | UI reference at `/docs/ui/{id}` |
| `api_docs.json` | `api_docs` | API reference at `/docs/api/{id}` |
Stream JSON holds fields, routes, source adapters, and optional UI admin config. See [Streams](/docs/core/streams) for the full schema.
## `streams/data/` — content
Filebase entries live beside stream definitions:
```text
streams/data/
├── docs/ # Hub guides (*.md)
├── core_docs/ # Core package docs
├── ui_docs/ # UI package docs
├── api_docs/ # API package docs
├── pages/ # HTML pages (*.html)
└── packages/ # Package catalog entries (if used)
```
Entry filenames become entry IDs (for example `introduction.md` → `/docs/introduction`).
## `resources/views/`
| Path | Used by |
|------|---------|
| `docs.blade.php` | All documentation routes |
| `page.blade.php`, `blank.blade.php` | Site pages stream |
| `partials/sidebar.blade.php` | Docs sidebar navigation |
| `partials/topbar.blade.php` | Site header |
## Application code
`AppServiceProvider` registers the admin panel:
```php
UI::panel(
Panel::make('admin')
->default()
->path('admin')
->brandName('Streams')
->middleware(['web'])
);
```
Most domain logic lives in the packages under `vendor/streams/`, which Composer installs as git clones from GitHub.
## Composer repositories
`composer.json` points each Streams package at its GitHub repository and pins the branch:
```json
{
"require": {
"streams/core": "dev-rc/prep as 2.0.x-dev",
"streams/ui": "1.0.x-dev"
},
"repositories": [
{ "type": "vcs", "url": "https://github.com/laravel-streams/streams-core.git", "no-api": true }
]
}
```
The `as 2.0.x-dev` alias lets packages that require `streams/core ^2.0` accept the branch. For local package work, `php scripts/composer-local.php` writes a gitignored `composer.local.json` with symlinked path repositories; see [Local development](/docs/local-development).
## Related
- [Site pages](/docs/site-pages) — how `pages.json` drives URLs
- [Content](/docs/content) — filebase formats and frontmatter
- [Contributing documentation](/docs/contributing-docs)
---
# Contributing: Site pages
> How pages.json and HTML entries drive site URLs without controllers.
Source: https://streams.dev/docs/site-pages
## Overview
Public site URLs (`/`, `/docs`, `/addons`) are served by the **pages** stream. No route files or controllers define these paths — the stream's `routes` block and entry frontmatter do.
Read this page when you need to add or change a marketing or landing page in this repository.
## Stream definition
From `streams/pages.json`:
```json
{
"routes": [
{
"handle": "view",
"uri": "{uri}",
"parse": true,
"view": "{layout}"
}
],
"fields": [
{ "handle": "id", "type": "slug", "required": true, "unique": true },
{ "handle": "title", "type": "string", "required": true },
{ "handle": "uri", "type": "string", "required": true, "unique": true },
{ "handle": "body", "type": "string" }
]
}
```
Key options:
- **`parse: true`** — registers one Laravel route per entry using each entry's `uri` field
- **`{uri}`** — route parameter bound to the entry's `uri` value
- **`{layout}`** — resolves the Blade layout from entry frontmatter
## Page entry format
Pages are HTML files in `streams/data/pages/` with YAML frontmatter:
```html
---
title: Documentation
uri: docs
layout: page
sort_order: 1
---
@include('partials.topbar')
{{ $entry->title }}
```
| Frontmatter key | Purpose |
|-----------------|---------|
| `uri` | URL path (`/` for homepage uses `uri: /` or root value) |
| `layout` | Blade layout name (`page`, `blank`, etc.) |
| `title` | Entry title, available as `$entry->title` in the body |
The homepage (`welcome.html`) uses `uri: /` and `layout: blank`.
## How routing works
1. At boot, Core reads `pages.json` and registers routes for each entry because `parse` is true.
2. A request to `/docs` matches the entry whose `uri` is `docs`.
3. `EntryController` renders the entry body through the layout named in frontmatter.
For programmatic routes outside the pages stream, use Laravel's `Route` facade or [Route::streams()](/docs/core/routes).
## Documentation vs site pages
| Stream | URL pattern | Content |
|--------|-------------|---------|
| `pages` | `/`, `/docs`, `/addons` | HTML with Blade |
| `docs` | `/docs/{id}` | Markdown hub guides |
| `core_docs` | `/docs/core/{id}` | Markdown package reference |
The `/docs` **index** is a pages entry (`docs.html`). Individual guide pages use the `docs` stream.
## Related
- [Content](/docs/content) — filebase formats
- [Routing](/docs/routing) — Laravel vs stream routes
- [Core routes reference](/docs/core/routes)
---
# Contributing: Package catalog
> How packages.json powers the add-ons page and homepage cards.
Source: https://streams.dev/docs/package-catalog
## Overview
The add-on catalog on `/addons` comes from the **packages** stream. Entries describe Streams ecosystem packages (Core, UI, API, SDK, and community addons) with Composer metadata and marketing copy.
## Stream source
`streams/packages.json` uses a **self** source — entries are stored inline in the JSON file under `data`, not as separate files:
```json
{
"config": {
"source": {
"type": "self"
}
}
}
```
Each entry includes fields such as `name`, `type`, `enabled`, and a `composer` object with package coordinates.
## Entry types
The `type` field groups catalog entries:
| Type | Examples |
|------|----------|
| `starter` | Official starter projects |
| `example` | Demo applications |
| `database` | Database adapters |
| `client` | API clients |
| `tools` | Dev tooling |
Only entries with `enabled: true` should appear in public listings (filter in your Blade/views as needed).
## Using entries in views
Pages can query the stream in Blade:
```blade
@foreach (Streams::entries('packages')->where('enabled', true)->get() as $package)
{{ $package->name }}
@endforeach
```
The homepage and `/addons` page include package cards that link to documentation or external repositories.
## Adding a catalog entry
1. Edit `streams/packages.json` and add an object to the `data` array.
2. Set a unique `id`, `name`, `type`, and `enabled`.
3. Add `composer` JSON with `name`, `description`, and repository URL if applicable.
4. Link to `/docs/{package}/introduction` when package docs exist on this site.
## Related
- [Addons](/docs/addons) — Composer addon discovery in Core
- [Core addons reference](/docs/core/addons)
- [Architecture](/docs/architecture)
---
# Contributing: Local development
> Install dependencies, run the dev server, and edit content locally.
Source: https://streams.dev/docs/local-development
## Prerequisites
- PHP 8.2+ with the extensions required by [Laravel 12](https://laravel.com/docs/12.x/deployment#server-requirements) (this site runs Laravel 12)
- Composer 2
- Node.js 20.19+ or 22.12+ and npm (for Vite and Tailwind)
- Git (Composer clones the Streams packages from GitHub)
## First-time setup
```bash
git clone https://github.com/laravel-streams/streams.dev.git --branch develop
cd streams.dev
composer install
cp .env.example .env
php artisan key:generate
npm install
composer dev
```
`composer dev` serves the app on `http://127.0.0.1:8427`, tails the application log with `php artisan pail`, and runs the Vite dev server (with hot reload) on port 5427. Both ports are strict: if one is taken the command stops instead of picking another. No database is needed.
Run the tests with `php artisan test` and `npm run test:js`.
## Editing content
Documentation and pages are flat files — no database required for content changes.
| Content | Edit path | Preview URL |
|---------|-----------|-------------|
| Hub guide | `streams/data/docs/{id}.md` | `/docs/{id}` |
| Core doc | `streams/data/core_docs/{id}.md` | `/docs/core/{id}` |
| Site page | `streams/data/pages/{id}.html` | entry `uri` |
| Stream config | `streams/{handle}.json` | after cache clear if cached |
In local environment, doc pages show an **Edit this page** link to that file on GitHub (`laravel-streams/streams.dev`, branch `develop`).
## Admin panel
Streams UI registers a panel at `/admin` from `AppServiceProvider`. Use it to browse and edit stream entries when you prefer a UI over flat files.
## Local package development
`composer.json` installs `streams/core` (branch `rc/prep`), `streams/ui` (`1.0`) and `streams/sdk` (`sdk/rc`) from their GitHub repositories as git clones in `vendor/streams/`.
To work on a package and see the change in this site, check it out next to this repository and link it in without editing `composer.json`:
```bash
composer local
```
`composer local` runs `scripts/composer-local.php`, which finds checkouts at `../_packages/streams-` or `../streams-` (or the path in `STREAMS__PATH`), writes a gitignored `composer.local.json` and `composer.local.lock` that symlink them, and then runs `composer update "streams/*"` against that file. `composer.json` and `composer.lock` stay as committed. Edits in the checkout then show up immediately.
The script warns when a checkout does not contain the branch `composer.json` asks for (for example `_packages/streams-core` on `2.0` while the site needs `rc/prep`). To link release-candidate worktrees from `../_rc/streams-` instead, ask for them by name:
```bash
composer local -- --rc=core,sdk # or: STREAMS_LOCAL_RC=core,sdk composer local
composer local -- --rc # every package from ../_rc
```
To only write `composer.local.json`, run `php scripts/composer-local.php` and then `COMPOSER=composer.local.json composer update "streams/*"` yourself. Run a plain `composer install` to switch back to the GitHub packages. Run package tests in the package directory; run `php artisan test` here for the site.
## Assets
Front-end assets compile through Vite:
```bash
npm run dev # watch mode
npm run build # production build
```
Styles live in `resources/css/` (`app.css` imports the tokens and components). Tailwind v4 runs through the `@tailwindcss/vite` plugin. The built files in `public/build` are committed, so run `npm run build` after changing CSS or JavaScript.
## Branches and deploys
Work on `develop` (or a short-lived branch off it that you merge back). When `develop` is ready, merge it into `master` and push both. `master` is the live site and GitHub's default branch, and `envoy run deploy` fast-forwards the server to it. The old `next`, `integration/rc` and `production` branches no longer exist.
## Related
- [Contributing documentation](/docs/contributing-docs)
- [This project](/docs/this-project)
- [Configuration](/docs/configuration)
---
# Contributing: Contributing documentation
> How to add or edit documentation on streams.dev.
Source: https://streams.dev/docs/contributing-docs
## Where docs live
All documentation for streams.dev is in **this repository** under `streams/data/`. Package repos link here; they do not maintain parallel doc trees.
| Section | Path | URL |
|---------|------|-----|
| Hub guides | `streams/data/docs/` | `/docs/{id}` |
| Core | `streams/data/core_docs/` | `/docs/core/{id}` |
| UI | `streams/data/ui_docs/` | `/docs/ui/{id}` |
| API | `streams/data/api_docs/` | `/docs/api/{id}` |
See [STYLE.md](https://github.com/laravel-streams/streams.dev/blob/develop/STYLE.md) for voice, frontmatter, and visual guidelines.
## Frontmatter
Every page requires YAML frontmatter:
```yaml
---
title: 'Core: Installation' # required; unique across the site (the page H1)
nav_title: Installation # short sidebar and browser-tab label
description: One sentence, plain text, for indexes and meta.
section: packages # get-started | guides | concepts | reference | packages | contributing
category: getting-started # hub docs only: a key in streams/docs_categories.json
package: core # core | ui | api | sdk | testing | client | site | all
order: 20 # sidebar order within the stream (hub docs: within the category); step by 10
tags: [core, installation]
status: ready # draft | review | ready | deprecated
---
```
The layout renders `title` as the page H1, so don't start the body with a `#` heading. Give every code fence a language, link to other pages with root-relative URLs (`/docs/core/fields`), and keep `json` blocks valid JSON. `tests/Feature/DocsContentTest.php` checks all of this.
**Status workflow:** `draft` → `review` → `ready` (and `deprecated` for pages kept only for old versions). Mark pages `ready` only after verifying claims against package source code.
## Hub vs section rules
- **Hub docs** explain workflows, architecture, and when to use a feature. Link to section docs for API depth.
- **Section docs** hold package-specific reference: classes, methods, configuration keys, examples.
- Do not duplicate long reference material in both places.
## Adding a hub page
1. Create `streams/data/docs/{slug}.md` with frontmatter including `category`.
2. Assign `order` within the category.
3. Add the page to a category in `streams/docs_categories.json` if introducing a new category.
4. Preview at `/docs/{slug}`.
## Adding a package reference page
1. Create `streams/data/{package}_docs/{slug}.md`.
2. Set `order` for sidebar ordering (no `category` field needed).
3. Sidebar picks up the page automatically from the doc stream entries.
## Accuracy standard
Document **what works today**. Do not describe APIs that are planned or commented out unless labeled as deferred.
Before marking `status: ready`, trace documented classes and methods to the package source. In this app, `vendor/streams/core`, `vendor/streams/ui`, and `vendor/streams/sdk` are symlinks into the package checkouts. `streams/api`, `streams/testing`, and the JavaScript client are not installed under `vendor/streams` here; read those checkouts directly. Do not edit files through `vendor/streams/*`. The `submodules/` directory in this repo is empty.
## Related
- [STYLE.md](https://github.com/laravel-streams/streams.dev/blob/develop/STYLE.md)
- [Project structure](/docs/project-structure)
- [Local development](/docs/local-development)