The interface contract
app:setup builds the interface in Vue or React, and both are the same application: same routes, same screens, same site JSON and same backend calls. This contract is what both honor. If one changes something here, the other changes the same way.
The source is the package's docs/shell-contract.md. This page covers all of it and ends with where the code departs from it.
Where each thing lives
stubs/app/common/ backend, shared by both
stubs/app/<ui>/ copied over the application root
package.json
vite.config.js
resources/views/app.blade.php
resources/<ui>/app/** the base application
resources/<ui>/routes.json placeholder; php artisan route:json rewrites it
resources/<ui>/index.js the LaraPack module (generated by larapack:import)
resources/<ui>/src/** its models: users and whatever is generated later
tests/Frontend/<ui>/ interface tests, outside what is copiedresources/<ui>/app/**belongs to laravel-setup when copied and to you afterwards.resources/<ui>/index.jsandresources/<ui>/src/**belong to LaraPack. The base application imports them and never edits them.
Boot
- axios.
axios.defaultswithwithCredentials,withXSRFToken,Accept: application/jsonandX-Requested-With: XMLHttpRequest. A single axios copy. - Backend routes.
setRoutes(routes.json)frominnoboxrr-route-resolver. No backend URL is written by hand: they all come fromroute(name). - Strings.
addTranslationswith the LaraPack module's and thenresources/<ui>/app/lang/*.json, in that order, andsetLocale(document.documentElement.lang). The base application's strings uset('English key'), with the translation inlang/es.json. - Styles.
innoboxrr-form-core/stylesand the module'ssrc/theme.js, if it exists. - Session and options. They load (
authandoptions) before the router mounts. - Toasts.
ToastRegionandConfirmHostare mounted once, at the root.
Front-end routes
| Path | Name | Access | Screen |
|---|---|---|---|
/ | site.home | Everyone | The site's home page |
/privacy | site.privacy | Everyone | The privacy page |
/terms | site.terms | Everyone | The terms page |
/contact | site.contact | Everyone | The contact page |
/join | site.join | Everyone | The join page |
/auth/login | auth.login | Guest | Log in; honors ?redirect= |
/auth/register | auth.register | Guest | Register |
/auth/forgot-password | auth.forgot-password | Guest | Request the link |
/auth/reset-password/:token/:email | auth.reset-password | Guest | New password; it is the URL in laravel-auth's email |
/admin | admin.dashboard | Session | Admin home |
/admin/profile | admin.profile | Session | Profile: name, email, photo and password |
/admin/site | admin.site | Admin | Site editor |
/admin/<model>/… | The module's | Session, and admin if in adminOnly | The routes the LaraPack module exports, as children of /admin |
| Anything else | not-found | Everyone | 404 |
Front-end route names do not clash with backend ones because they live in another registry: the front-end router versus routes.json.
Guards
| Flag | Vue | React | Effect |
|---|---|---|---|
| Guest | meta.guest | handle.guest | A user with a session goes to /admin. |
| Session | meta.auth | handle.auth | A visitor goes to /auth/login?redirect=<route>. Module routes carry auth: true. |
| Admin | meta.admin, or the name in adminOnly | handle.admin, or the id in adminOnly | Requires is_admin; without it, to /admin with a toast. |
- Guards look at the whole route chain: a child inherits what its parent requires.
?redirect=only accepts internal paths: they start with/and not//or/\.
resources/<ui>/app/config.js
Exports adminOnly: the names (Vue) or ids (React) of the module routes only an admin sees. By default, the users one (AdminUsers).
The admin menu
It is built, not written. Every first-level module route with a title (meta.title / handle.title) and no parameters is an entry. Those in adminOnly only show to an admin. Plus:
- Home (
admin.dashboard), always first. - "Administration" group, admins only: the
adminOnlyroutes, "Site" (admin.site), "Logs" (/log-viewer, new tab) and "Environment" (/env-editor, new tab).
A model generated later shows up in the menu on its own at build time.
State
auth
load(). GETroute('auth.get.auth')→{ user, authenticated, is_admin, verified, impersonating }.verifiedistruefor a user that does not implementMustVerifyEmail(laravel-auth 6.0.2): the verification notice only shows when there really is an email to verify.login({ email, password, remember }). First GET/sanctum/csrf-cookie(route('sanctum.csrf-cookie')if it is inroutes.json), then theauth.loginPOST, thenload().- The rest.
register,logout,forgotPassword,resetPassword,updatePassword,resendVerificationandrevertImpersonationuse laravel-auth 6's routes, with dotted names:
| Action | Route |
|---|---|
register | auth.register |
logout | auth.logout |
forgotPassword | auth.forgot.password |
resetPassword | auth.reset.password |
updatePassword | auth.update.password |
resendVerification | auth.email.verification.notification |
revertImpersonation | auth.revert.impersonate |
revertImpersonation(). POST toauth.revert.impersonate, thenload(). laravel-auth 6.1 no longer accepts GET, which another site could trigger with an<img>. It carries the CSRF token like any other POST.?redirect=. Only accepts internal paths.- Errors. A 401 outside
load()clears the session and sends to login. A 419 fetches the CSRF cookie again and retries the request once.
options
load(). GETroute('api.laravel-options.option.index', { paginate: 0 }), which is public. Storeskey → valueand each option'sid. Avaluethat is a JSON object or array is stored decoded; any other text stays text (asite_nameof"2024"does not become a number), just likeOption::value()in the backend.option(path, default).option('site_name')oroption('theme.home'), which walks into the JSON by dots.save(key, value). Calls laravel-options' update (admin only) withoption_id,name,keyand thevalue, serialized to JSON when it is not text. If the option does not exist yet, calls its create. Then updates the state.
notifications (laravel-notifications 2.1)
- Count. The unread ones, when the admin panel mounts, every 60 s and when the tab becomes visible.
- List. The latest ones, when the bell opens.
- Marking. Marking one as read navigates to
data.action: an internal path with the router, an absolute URL withlocation. All can be marked at once. - Text.
data.messageis rendered as text, never as HTML.
The admin panel
A layout with fe-shell: a header (site name; menu button on mobile, with a backdrop that closes it; bell; dark mode; user menu) and a sidebar with the menu.
- Impersonation notice, when
impersonating: "You are viewing the account of …" and "Back to my account". - Verification notice, when
verified === false: resend the email. - User menu: Profile and Sign out.
- Dark mode:
data-themeon<html>, remembered inlocalStorage. With no choice, the system's. - Home: a greeting and one card per menu entry.
- Profile.
- Name and email through the generated user's
update:api.app.user.update, fieldsuser_id,nameandemail. - Photo uploaded with laravel-uploads (
lu.upload.file, fieldfile) and saved as theavatarmeta through the same update, using the upload's relativeuri. Removing it sendsavatar: ''. - Password through laravel-auth's
update-password.
- Name and email through the generated user's
- Site editor (see below).
Details in The admin panel and Authentication and users.
The site
Pages render from the theme option, seeded by SiteOptionsSeeder. It is the old theme-manager's format, so an older application's content still renders:
{
"home": {
"title": "Inicio",
"sections": [
{ "theme": "legacy", "group": "hero", "name": "HeroOne", "props": { "display": true, "title": "..." } }
]
},
"privacy": { "title": "...", "sections": [] },
"terms": { "title": "...", "sections": [] },
"contact": { "title": "...", "sections": [] },
"join": { "title": "...", "sections": [] }
}- Registry. Each section is looked up by
<theme>/<group>/<name>. One that does not exist does not render and logs a warning. display. A section withprops.displayset tofalseor"false"does not render; without the key, it does.- Title. The tab title is
<page title> · <site_name>. - Images. Every image is optional: without it the section still looks right, with no broken gaps.
- Styles. They come from form-core's variables (
--fe-*), so the site has dark mode. No Tailwind, no Headless UI, no Heroicons: icons use form-elements'Icon. - Links. One that starts with
/navigates with the router; anything else is a normal link.
Sections and their props
| Section | Props |
|---|---|
legacy/header/HeaderOne | logo, nav: [{ label, link }], facebook, twitter, instagram, youtube, whatsapp, linkedin, tiktok. Shows "Sign in" or "Administrator" depending on the session. |
legacy/hero/HeroOne | badge, badge_value, badge_link, title, message, primary_button_text, primary_button_link, secondary_button_text, secondary_button_link, videos: [url] (one at random) |
legacy/hero/HeroTwo | HeroOne's without videos, plus badge_value_link, video, display_play_button, play_button_text, imgs_1, imgs_2, imgs_3: [url] |
legacy/hero/HeroThree | HeroOne's without videos |
legacy/section/MissionSection | title, subtitle, message, button_text, button_link, images: [url] (up to 4) |
legacy/section/JoinSection | title, subtitle, image, features: [text], button_text, button_link |
legacy/section/FaqSection | title, subtitle, items: [{ question, answer }] |
legacy/section/PartnersSection | title, items: [{ name, logo, link }] |
legacy/section/TestimonialsSection | title, subtitle, feature: { body, author: { name, handle, image } }, items: [{ body, author: { name, handle, image } }] |
legacy/section/PlansSection | title, subtitle, frequencies: [{ value, label, price_suffix }], tiers: [{ id, name, href, description, price: { <frequency>: text }, features: [text], most_popular }] |
legacy/section/HtmlContent | content: HTML written by the admin |
legacy/footer/FooterOne | logo, description, cols: [{ title, items: [{ name, link }] }], newsletter: { title, subtitle, button_text, button_link }, social_links: { facebook, instagram, twitter, github, youtube, linkedin, tiktok, whatsapp } |
legacy/cookie-consent/CookieConsentOne | message, accept_text, reject_text, policy_link. Stores the decision in the cookie_consent cookie (accepted or rejected) and does not show again. |
Every prop, with its type and behavior, is in The site and its editor.
The site editor
/admin/site, admins only:
- General:
site_nameandsite_description. - Pages: one tab per page. In each, its sections in order: toggle (
display), move up, move down, remove, and add one from the registry. - Props: each section's, in a JSON editor (CodeMirror,
json). Invalid JSON is flagged and blocks saving. - "View page" opens the page in a new tab.
- Save writes the
themeoption withoptions.saveand confirms with a toast. A 422 shows the error.
Where the code departs from the contract
In laravel-setup 7.0.1, the contract does not capture these points:
- Option update in Vue. Vue sends only
{ option_id, value }and React sends all four fields. It works because in laravel-optionskeyissometimesandnameisnullable. - Settings. The contract only names
adminOnly. React also exportsadminBase,userUpdateRoute,adminTools,notificationsIntervalandsitePagesfromconfig.js; Vue has those values written inrouter/index.js,router/menu.js,admin/ProfileView.vueandstores/notifications.js. - Behavioral differences. React's error screen, different string keys ("Sign in" / "Log in"), the name of the option that skips the 401 redirect (
skipAuthHandling/skipAuthRedirect) and more. They are in Customize and extend. - What is not wired. Social login, starting an impersonation, auditing and the S3 file manager have no interface. See What it includes.