Skip to content

Start Building ​

This guide walks through the supported, opinionated starting point for a VUEDA-based project, focusing on the minimum steps needed to get a VUEDA API server and a VUEDA client application talking to each other.

If you already have an existing Django or Vue project, this guide is still useful as a reference for how VUEDA expects projects to be structured, but it does not attempt to provide conversion steps.

By the end of this guide, you will have a running Django API and Vue client connected via VUEDA, exposing a simple inventory model end-to-end.

For a more complete example, try Widget Warehouse, a VUEDA application with inventory, purchasing workflows, and custom dashboards. Its repository includes the source and a guided walkthrough.

Prerequisites ​

  • Python 3.12+: for running the VUEDA Server
  • Node.js 22+: for running the VUEDA Client
  • A PostgreSQL database: for hosting your application data
  • A Redis server: for the cache that holds sign-in sessions (or use the database cache instead; see Configure Local Settings)
  • Git: for version control
  • Copier: for scaffolding from the project templates
  • uv: for Python dependency management
  • pnpm: for Node.js dependency management

Optional ​

  • just: for common developer CLI tooling (included in the DX template)

TIP

Run the server under an ASGI server instead of Django's runserver. The DX template's just serve runs gunicorn with uvicorn workers, the same setup as production. VUEDA's planned websocket support will also need ASGI. Both templates already depend on gunicorn and uvicorn. With the minimal template, run this from server/: uv run gunicorn config.asgi -k uvicorn.workers.UvicornWorker --reload --bind localhost:8000.

Environment Setup ​

This guide assumes access to a bash-like shell (Linux, macOS, WSL2, etc.) for running commands. Adjust accordingly for other environments (PowerShell, cmd.exe, etc.).

Package Registry Access ​

VUEDA is available from public PyPI and public npm. No registry credentials are needed.

The v3 series is a prerelease, and the templates select it with version ranges. To add VUEDA to an existing project, use the same ranges. For the server, run uv add "vueda[redis]>=3.0.0b1,<4". For the client, run pnpm add "@arrai-innovations/vueda@^3.0.0-beta.1". A range names the release series directly, so it does not depend on which version a registry tag such as latest points to.

Scaffold a New Project ​

VUEDA provides two Copier templates for scaffolding a new Integrator project:

  • integrator-monorepo: minimal setup with direct uv/pnpm workflows.
  • integrator-monorepo-dx: DX-focused setup with repository automation via just (includes linting, formatting, git hooks, and just serve for running both servers concurrently).

Clone the VUEDA repository, then copy one template from the clone:

console
git clone https://github.com/arrai-innovations/vueda.git

# DX template (recommended)
uvx copier copy vueda/templates/integrator-monorepo-dx ./your-project

# or: minimal template
uvx copier copy vueda/templates/integrator-monorepo ./your-project

Copier will prompt you for a project name, slug, ports, and other options. The defaults are sensible for most setups.

The template generates:

  • A Django server under server/ with VUEDA wired into settings, URLs, and config
  • A custom users app under server/<your_package>/users/ with a project-specific User model extending VUEDA's base user (required by AUTH_USER_MODEL)
  • Two TOML config files: server/config.toml (shared, safe to commit) and server/config.local.toml (secrets, do not commit)
  • A Vue client under client/ with VUEDA's router and action system bootstrapped
  • A uv workspace root and pnpm workspace definition
  • (DX template only) A Justfile, lefthook config, ruff, eslint, and prettier setup

Initialize the Repository ​

The DX template includes lefthook for git hooks, which runs automatically during pnpm install. Initialize a git repository before installing dependencies:

console
cd your-project
git init

Install Dependencies ​

DX template ​

console
just bootstrap

Minimal template ​

console
uv sync --all-packages
pnpm install

Configure Local Settings ​

VUEDA projects use a two-file TOML configuration system, both under server/:

  • config.toml: shared settings safe to commit (allowed hosts, frontend URL, app registry, CORS origins, etc.). The template ships placeholder public-deployment defaults; replace them with your real deployment domain before production use.
  • config.local.toml: local-only overrides and secrets (do not commit). This is where machine-specific values like database credentials, local frontend origins, and DEBUG belong.

Settings in config.local.toml override those in config.toml. Both files are loaded by TomlEnv in server/config/settings/base.py and consumed by VUEDA's core.default_settings.get_defaults, which sets up Django settings (INSTALLED_APPS, DATABASES, CACHES, middleware, auth, etc.) from these keys.

Before starting the server, open server/config.local.toml and set real values:

toml
SECRET_KEY = "a-real-secret-key"
DEBUG = true
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
FRONTEND_DOMAIN = "http://localhost:5173"
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"]
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]
DATABASE_URL = "postgres://postgres:postgres@localhost:5432/your-project"
CACHE_URL = "redis://localhost:6379/0?key_prefix=your-project-"

The template pre-populates the local host/origin values from the bind IP and client port you chose during scaffolding, and pre-populates DATABASE_URL with a reasonable guess based on your project slug. Update those values if your local network or Postgres connection details differ. SECRET_KEY should be changed from the placeholder for any non-trivial use.

Sign-in sessions live in the cache that CACHE_URL names. To skip Redis, set CACHE_URL = "db://your_project_cache" and run manage.py createcachetable after migrate in the next step. See Configure the Cache and Sessions for the options.

TIP

The template's config.toml also registers the scaffolded users app via LOCAL_APPS and sets AUTH_USER_MODEL = "users.User". These are required for VUEDA's user system to work. You can add your own apps to LOCAL_APPS or append to INSTALLED_APPS directly in base.py (the guide uses the latter approach below).

First Contact ​

At this point we have a minimal VUEDA server and client setup. Let's verify that everything is wired up correctly.

TIP

The remainder of this guide uses localhost for simplicity. If you are working in WSL2, Docker containers, or other networked environments, you may need to adjust hostnames or use 0.0.0.0 for binding.

First, apply database migrations:

console
# DX template
just manage migrate

# Minimal template
cd server
uv run python manage.py migrate

Now start the server and client. With the DX template, run both concurrently:

console
just serve

With the minimal template, start each in its own terminal:

console
# Terminal 1: server
cd server
uv run gunicorn config.asgi -k uvicorn.workers.UvicornWorker --reload --bind localhost:8000
console
# Terminal 2: client
cd client
pnpm dev

The client dev server defaults to port 5173 (configurable via the copier options).

Now verify the server is responding:

console
curl -i http://localhost:8000/routes/vueda.user/who-is/

If the server is running and you are not signed in, you get a 200 response with the body {"auth_pending_flow":null}.

For the client, open your browser and navigate to http://localhost:5173. The template's router redirects / to the welcome route. That route requires a signed-in user, so it sends you to the Sign In form at /sign-in/. The browser console should show no errors. To create a user to sign in with, follow Verify the New API Endpoints later in this tutorial.

TIP

If you want your local environment to match production security settings (secure session and CSRF cookies, HTTPS-only), see Local HTTPS Development.

WARNING

If you are having issues from here, consult Django or Vite documentation for troubleshooting tips, or a system administrator for networking problems, as the issues are likely outside the scope of this guide.

VUEDA Server ​

With the boilerplate in place, we can now start building on the VUEDA Server. For this guide, we will add several simple models in the same Django app to demonstrate VUEDA's capabilities.

Create a Django App ​

Add a new Django app under the project namespace. In server/your_project/, create a new folder called inventory with the following files:

  • __init__.py
  • apps.py
  • models.py
  • serializers.py
  • viewsets.py
  • filtersets.py
  • routers.py
  • urls.py

Models ​

VUEDA provides its own extensions of Django's Model class:

  • VuedaModel: adds an expected VuedaModel.formatted_name GeneratedField (by default based on a model's name field) and a custom BaseModelMeta class that sets up default permissions in VUEDA's expected way.
  • Lookup: extends VuedaModel with a unique code field, intended for lightweight, potentially user-defined, reference data tables.

IMPORTANT

VUEDA uses create, read, update, delete, and list permissions, which aligns better with djangorestframework's viewset actions than Django's default add, change, delete, and view permissions. All VUEDA models must therefore inherit from VuedaModel to ensure proper permission handling, and must have a class Meta(VuedaModel.Meta) (or equivalently, class Meta({@api py:class:vueda.core.models.BaseModelMeta})) by default.

server/your_project/inventory/models.py:

python
from django.db import models

from vueda.core.models import BaseModelMeta, Lookup, VuedaModel


class Product(VuedaModel):
    name = models.CharField(max_length=255)
    sku = models.CharField(max_length=64, unique=True)
    description = models.TextField(blank=True)

    class Meta(BaseModelMeta):
        ordering = ["name", "id"]


class OptionType(Lookup):
    class Meta(BaseModelMeta):
        ordering = ["name", "id"]


class ProductOption(VuedaModel):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="options")
    option_type = models.ForeignKey(OptionType, on_delete=models.PROTECT, related_name="product_options")
    name = models.CharField(max_length=255)
    value = models.CharField(max_length=255)
    sort_order = models.PositiveIntegerField(default=0)

    class Meta(BaseModelMeta):
        ordering = ["sort_order", "id"]

Serializers ​

VUEDA provides VuedaSerializer and VuedaLookupSerializer base classes for DRF serializers. VuedaLookupSerializer handles the boilerplate around the code field for Lookup models.

IMPORTANT

As with models, all VUEDA serializers should have a class Meta({@api py:class:vueda.core.serializers.VuedaSerializer}.Meta) or class Meta({@api py:class:vueda.core.serializers.VuedaLookupSerializer}.Meta) to ensure proper default behavior.

server/your_project/inventory/serializers.py:

python
from vueda.core.serializers import VuedaLookupSerializer, VuedaSerializer

from your_project.inventory.models import OptionType, Product, ProductOption


class ProductSerializer(VuedaSerializer):
    class Meta(VuedaSerializer.Meta):
        model = Product
        fields = [
            "id",
            "name",
            "sku",
            "description",
            "formatted_name",
            "available_actions",
        ]


class OptionTypeSerializer(VuedaLookupSerializer):
    class Meta(VuedaLookupSerializer.Meta):
        model = OptionType
        fields = VuedaLookupSerializer.Meta.fields


class ProductOptionSerializer(VuedaSerializer):
    class Meta(VuedaSerializer.Meta):
        model = ProductOption
        fields = [
            "id",
            "product",
            "option_type",
            "name",
            "value",
            "sort_order",
            "formatted_name",
            "available_actions",
        ]

Viewsets ​

VUEDA provides a VuedaViewSet base class which:

  • sets up default behavior for CRUDL actions
  • integrates with VUEDA's permission system
  • extends DRF's ModelViewSet to cause more intentional errors when passing extra query parameters or fields (rather than silently ignoring them)
  • provides row-level filtering hooks
  • integrates and extends drf-flex-fields2, adding permit_{action}_expands for every action. The library supports only permit_list_expands, which defaults to an empty list, so a list request expands nothing until the viewset names its expands

server/your_project/inventory/viewsets.py:

python
from vueda.core.viewsets import VuedaViewSet

from your_project.inventory.filtersets import (
    OptionTypeFilterSet,
    ProductFilterSet,
    ProductOptionFilterSet,
)
from your_project.inventory.models import OptionType, Product, ProductOption
from your_project.inventory.serializers import (
    OptionTypeSerializer,
    ProductOptionSerializer,
    ProductSerializer,
)


class ProductViewSet(VuedaViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filterset_class = ProductFilterSet


class OptionTypeViewSet(VuedaViewSet):
    queryset = OptionType.objects.all()
    serializer_class = OptionTypeSerializer
    filterset_class = OptionTypeFilterSet


class ProductOptionViewSet(VuedaViewSet):
    queryset = ProductOption.objects.all()
    serializer_class = ProductOptionSerializer
    filterset_class = ProductOptionFilterSet

Filtersets ​

VUEDA provides VuedaFilterSet as a base for DRF filtersets.

server/your_project/inventory/filtersets.py:

python
from vueda.core.filters import VuedaFilterSet

from your_project.inventory.models import OptionType, Product, ProductOption


class ProductFilterSet(VuedaFilterSet):
    class Meta:
        model = Product
        fields = ["id", "name", "sku"]


class OptionTypeFilterSet(VuedaFilterSet):
    class Meta:
        model = OptionType
        fields = ["id", "code", "name"]


class ProductOptionFilterSet(VuedaFilterSet):
    class Meta:
        model = ProductOption
        fields = ["id", "product", "option_type", "name"]

Router and URLs ​

VUEDA provides VuedaRouter, which builds on DRF's SimpleRouter to generate standard CRUDL routes, namespaces route names with the app label, and supports Bulk Actions via @action(bulk=True).

server/your_project/inventory/routers.py:

python
from vueda.core.routers import VuedaRouter

from your_project.inventory.viewsets import (
    OptionTypeViewSet,
    ProductOptionViewSet,
    ProductViewSet,
)

router = VuedaRouter()
router.register(r"product", ProductViewSet)
router.register(r"optiontype", OptionTypeViewSet)
router.register(r"productoption", ProductOptionViewSet)
urlpatterns = router.urls

server/your_project/inventory/urls.py:

python
from django.urls import include, path

from your_project.inventory.routers import urlpatterns

urlpatterns = [
    path("", include(urlpatterns)),
]

Finally, wire the inventory URLs into your project's namespace URL file. The copier template generates server/your_project/urls.py with an empty urlpatterns. Add the inventory app:

python
from django.urls import include, path

urlpatterns = [
    path("inventory/", include("your_project.inventory.urls")),
]

The template's server/config/urls.py already includes your project namespace under the routes/ prefix, so the inventory endpoints will be available at /routes/inventory/. The client requests each model at /routes/<app_label>/<model_name>/, so each router prefix is the model name. Router and URL Wiring describes this rule.

App Configuration and Model-Info Registration ​

VUEDA's client discovers models through a metadata API. For your models to appear in this API (and therefore be usable by the client), you need to register them with VUEDA's info.registration.register function in the app's AppConfig.ready() method.

server/your_project/inventory/apps.py:

python
from django.apps import AppConfig


class InventoryConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "your_project.inventory"

    def ready(self):
        from vueda.info import register

        from .serializers import (
            OptionTypeSerializer,
            ProductOptionSerializer,
            ProductSerializer,
        )
        from .viewsets import (
            OptionTypeViewSet,
            ProductOptionViewSet,
            ProductViewSet,
        )

        register(ProductSerializer, ProductViewSet)
        register(OptionTypeSerializer, OptionTypeViewSet)
        register(ProductOptionSerializer, ProductOptionViewSet)

The imports are inside ready() deliberately. Registration resolves Content Types internally, which requires the Django app registry to be fully initialized first.

IMPORTANT

Without info.registration.register, the model's API endpoints will work (you can still curl them), but the client will not be able to discover the model's fields, actions, or permissions. This is the most common cause of "model doesn't show up in the client."

Register the App ​

Add the new app to INSTALLED_APPS. The copier template's settings use core.default_settings.get_defaults from VUEDA, which assembles INSTALLED_APPS. That list holds Django's own apps, the default VUEDA_APPS list, third-party apps, and any apps named in LOCAL_APPS in config.toml. The scaffolded users app is already there. You need to add your new app as well.

The default VUEDA_APPS list installs every VUEDA app, and two of them are optional. Django App Boundaries separates required infrastructure from optional feature apps and gives the combinations VUEDA tests.

In server/config/settings/base.py, after the locals().update(get_defaults(env)) line, add:

python
INSTALLED_APPS += ["your_project.inventory"]

Alternatively, you could add the app to LOCAL_APPS in config.toml. Either approach works; INSTALLED_APPS += in base.py keeps the registration close to the code, while LOCAL_APPS keeps it in config.

Then run migrations:

console
cd server
uv run python manage.py makemigrations inventory
uv run python manage.py migrate

Verify the New API Endpoints ​

VUEDA enforces CRUDL permissions by default. Each model gets five permissions: inventory.create_product, inventory.read_product, inventory.update_product, inventory.delete_product, and inventory.list_product, and the same five for optiontype and productoption.

The inventory app has more permissions than these 15. VUEDA records each model's history in an event model (productevent, optiontypeevent, and productoptionevent), and each event model gets its own five permissions. Reading a product's history needs only inventory.read_product, so the group below leaves the event model permissions out.

The group also needs contenttypes.read_contenttype and contenttypes.list_contenttype. The client reads each model's metadata (Model Info) before it opens the model, and those two permissions gate that metadata. Model Info Access explains this gate.

The script below creates a group with the 15 model permissions and the two model info permissions, contenttypes.read_contenttype and contenttypes.list_contenttype. It then creates a user in that group:

console
uv run python manage.py shell -c '
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission

model_permissions = Permission.objects.filter(
    content_type__app_label="inventory",
    content_type__model__in=["product", "optiontype", "productoption"],
)
model_info_permissions = Permission.objects.filter(
    content_type__app_label="contenttypes",
    codename__in=["read_contenttype", "list_contenttype"],
)
group, _ = Group.objects.get_or_create(name="Inventory Editors")
group.permissions.set([*model_permissions, *model_info_permissions])
user = get_user_model().objects.create_user(email="you@example.com", password="your-password", name="You")
user.groups.add(group)
'

Then, in a new terminal, log in via curl and store the session cookie. The login endpoint sets a CSRF cookie in its response, which you will need for subsequent mutating requests.

TIP

The CSRF cookie name is project-specific (<project-slug>-csrf-token by default, configured via CSRF_COOKIE_NAME in local.py). The examples below use your-project-csrf-token as a placeholder; substitute your actual project slug.

console
COOKIE_JAR=/tmp/vueda-cookies.txt
CSRF_COOKIE=your-project-csrf-token

# Log in (uses email + password; sets CSRF cookie in the response)
curl -c $COOKIE_JAR \
  -H "Content-Type: application/json" \
  -X POST http://localhost:8000/routes/vueda.user/login/ \
  -d '{"email":"you@example.com","password":"your-password"}'

# Extract the CSRF token for subsequent requests
CSRF_TOKEN=$(awk -v name="$CSRF_COOKIE" '$6 == name {print $7}' $COOKIE_JAR)

If the login succeeded, who-is should now return your user info:

console
curl -b $COOKIE_JAR http://localhost:8000/routes/vueda.user/who-is/

Now test CRUDL on the inventory endpoints:

console
# Create
curl -b $COOKIE_JAR -c $COOKIE_JAR \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: $CSRF_TOKEN" \
  -X POST http://localhost:8000/routes/inventory/product/ \
  -d '{"name":"Starter Kit","sku":"STARTER-001","description":"Demo product"}'
# Expect: 201 with the created object

# List
curl -b $COOKIE_JAR http://localhost:8000/routes/inventory/product/
# Expect: 200 with a list including the created object

# Retrieve
curl -b $COOKIE_JAR http://localhost:8000/routes/inventory/product/1/
# Expect: 200 with the created object

# Partial update
curl -b $COOKIE_JAR -c $COOKIE_JAR \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: $CSRF_TOKEN" \
  -X PATCH http://localhost:8000/routes/inventory/product/1/ \
  -d '{"description":"Updated description"}'
# Expect: 200 with the updated object

# Delete: create a second product, then delete it
curl -b $COOKIE_JAR -c $COOKIE_JAR \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: $CSRF_TOKEN" \
  -X POST http://localhost:8000/routes/inventory/product/ \
  -d '{"name":"Spare Kit","sku":"SPARE-001"}'
curl -b $COOKIE_JAR -c $COOKIE_JAR \
  -H "X-CSRFToken: $CSRF_TOKEN" \
  -X DELETE http://localhost:8000/routes/inventory/product/2/
# Expect: 204 with no content; Starter Kit remains

VUEDA Client ​

The scaffolded client has Vue, Pinia, vue-router, and VUEDA's action router wired up. Next, check the server connection and client setup, then fill in CRUDL View Resolution.

Connect to the Server ​

During local development the client dev server and Django run on different ports. The scaffolded client/.env.development already contains VITE_DJANGO_CONNECTION_PORT set to the port you chose during scaffolding, so VUEDA knows where to reach the Django server. No Vite proxy is needed; the template's config.local.toml already includes the local client origin in CORS_ALLOWED_ORIGINS.

Check the Client Setup ​

The scaffolded client/src/index.css and client/src/main.js already set up the client, so keep both files. index.css loads Tailwind, the theme's base.css tokens, and the fonts. main.js registers the Tailwind theme, the icons, and the CRUDL data adapters. See Client Plugin Prerequisites for what each call does.

TIP

For per-family or per-component loading and the order required for project patches, see How the theme is registered.

Check the Sign-In and Welcome Routes ​

The scaffolded client/src/router/index.js already routes sign-in to ViewSignIn, VUEDA's ready-made sign-in form. After sign-in, the form sends the user to the welcome route, which shows the scaffolded client/src/views/ViewWelcome.vue. Replace that view with your application's own home page when you have one. The router also has routes for the password reset, two-factor, and reauthentication pages. To build your own forms, see Build Auth Views.

Configure CRUDL View Resolution ​

The scaffolded router passes an empty crudComponents object to setCrudComponents, so no CRUDL route has a view yet. In client/src/router/index.js, replace this line:

javascript
const crudComponents = {};

with this map:

javascript
const crudComponents = {
    list: async () => (await import("@vueda/views/ViewList.vue")).default,
    create: async () => (await import("@vueda/views/ViewCreate.vue")).default,
    read: async () => (await import("@vueda/views/ViewRead.vue")).default,
    update: async () => (await import("@vueda/views/ViewUpdate.vue")).default,
    destroy: async () => (await import("@vueda/views/ViewDestroy.vue")).default,
};

Keep the rest of the router as it is.

crudComponents maps each CRUDL action to a built-in view (ViewList, ViewCreate, ViewRead, ViewUpdate, ViewDestroy). ViewActionRouter uses this map to resolve which component to render. These views auto-discover fields, filters, and permissions from Model Info, so no per-model client code is needed for a working baseline. See Routing and View Resolution for the full resolution chain.

Per-model view overrides

In a real project you may want a custom view for a specific model. The common pattern is a dynamic import with a fallback:

javascript
list: async ({ app, model }) => {
    try {
        return (await import(`@/views/ViewList${pascal(app)}${pascal(model)}.vue`)).default;
    } catch {
        return (await import("@vueda/views/ViewList.vue")).default;
    }
},

This lets you drop in a ViewListInventoryProduct.vue for one model while every other model keeps the default. See Creating a CRUDL Surface for details.

Verify in the Browser ​

Start both servers if they are not already running:

console
# DX template
just serve

# Minimal template (two terminals)
cd server && uv run gunicorn config.asgi -k uvicorn.workers.UvicornWorker --reload --bind localhost:8000
cd client && pnpm dev

Open http://localhost:5173/sign-in/ in your browser.

  1. You should see the Sign In form.
  2. Sign in as you@example.com with the password you set earlier.
  3. After login you should land on /welcome/.
  4. Navigate to http://localhost:5173/inventory/product/list/. The Starter Kit product you created with curl appears here. To reach other models, the client URL pattern is /{app_label}/{model}/list/, where {model} comes from model-info and follows Django's model_name convention: the class name lowercased with no separators. For example, ProductOption becomes productoption, so its list URL is /inventory/productoption/list/. The server router registers each model under the same name (see Router and URLs).
  5. Use the "Create" action to add a product and verify it appears in the list.
  6. Open http://localhost:5173/inventory/product/read/1 to see the Starter Kit read view, then try update and destroy from there. The next section links list rows to this view.

Customize with Model Config ​

The built-in views render every field the serializer exposes. To adjust which fields appear, set sort defaults, or reorder columns without building custom views, use storeModelConfig.

Create client/src/setupModelConfig.js:

javascript
import { storeModelConfig } from "@vueda/stores/storeModelConfig.js";

export function setupModelConfig() {
    const modelConfig = storeModelConfig();

    modelConfig.setConfig(
        { app: "inventory", model: "product" },
        {
            displayFields: ["name", "sku", "description"],
            sorted: ["name"],
            detailLinkField: "name",
        },
        {
            create: {
                fields: ["name", "sku", "description"],
            },
        },
    );
}

Then call it from main.js after app.use(pinia):

javascript
import { setupModelConfig } from "./setupModelConfig.js";

// ... after app.use(pinia)
setupModelConfig();

detailLinkField turns each row's name into a link to that product's read or update view; see Link List Rows to Detail Views. The fields shorthand sets displayFields, fetchFields, and submitFields together. Per-view configs (keyed by action name) merge on top of the generic config. See Configure CRUDL Views for all available options.