Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Lighthouse Graphql Multi Schema Laravel Package

yakovenko/laravel-lighthouse-graphql-multi-schema

Add multiple GraphQL schemas to Laravel Lighthouse with separate schema files, routes, namespaces, and middleware. Build distinct API surfaces in one Laravel app while sharing the same codebase and core services.

View on GitHub
Deep Wiki
Context7

Getting Started

The package extends Lighthouse's schema configuration so a single Laravel application can expose several GraphQL endpoints — typically a public API and an admin/internal API, or one schema per tenant. Install it alongside Lighthouse:

composer require nuwave/lighthouse yakovenko/laravel-lighthouse-graphql-multi-schema
php artisan vendor:publish --tag=lighthouse-config
php artisan vendor:publish --provider="Yakovenko\\LighthouseMultiSchema\\LighthouseMultiSchemaServiceProvider"

Look at three files first, in this order:

  1. config/lighthouse.php → the schemas array. Lighthouse already supports named schemas here; this package layers registration, routing, and per-schema middleware on top. Confirm the exact shape the package expects (uri, schema_path, middleware, guard) against its README before copying snippets.
  2. The package config (usually config/lighthouse-multi-schema.php or a schemas key published by the provider) — this is where you declare schema names, URIs, SDL paths, and per-schema middleware/guards.
  3. The package service provider — check whether schemas are registered at boot time (static, route-cacheable) or resolved per request (tenant-driven). This determines your whole deployment story.

First use case — split public and admin:

// config/lighthouse-multi-schema.php (shape may differ; verify with README)
'schemas' => [
    'public' => [
        'uri'         => '/graphql',
        'schema_path' => base_path('graphql/public.graphql'),
        'middleware'  => ['api', 'throttle:60,1'],
        'guard'       => 'api',
    ],
    'admin' => [
        'uri'         => '/admin/graphql',
        'schema_path' => base_path('graphql/admin.graphql'),
        'middleware'  => ['api', 'auth:sanctum', 'can:access-admin-api'],
        'guard'       => 'sanctum',
    ],
],

Then define graphql/public.graphql and graphql/admin.graphql, add a trivial type Query { ping: String! @field(resolver: "App\\GraphQL\\Queries\\Ping") }, and verify with an introspection POST to each URI (or php artisan lighthouse:print-schema --schema=admin where supported). Ship the split before you add business types — the auth boundary is the part you want proven early.

Implementation Patterns

Shared base types, divergent roots. Keep reusable scalars, interfaces, and enums in graphql/base/*.graphql and #import them from each schema. Duplicating type User across schemas is the fastest way to drift; a single @interface-free shared file imported by both keeps resolvers identical while each schema controls which fields and mutations are reachable.

# graphql/public.graphql
#import base/scalars.graphql
#import base/user.graphql
#import public/queries.graphql

# graphql/admin.graphql
#import base/scalars.graphql
#import base/user.graphql
#import admin/queries.graphql
#import admin/mutations.graphql

Auth boundaries via schema middleware, not directives alone. @guard(with: "sanctum") protects a field, but schema-level middleware rejects unauthenticated traffic before parsing — cheaper and less error-prone. Pair the route middleware with AttemptAuthentication in the schema's middleware list so auth() is populated for resolvers.

Schema-per-tenant at request time. If the package supports runtime registration, resolve the tenant in middleware, validate the name against an allow-list, and swap the schema for that request:

final class ResolveTenantSchema
{
    public function handle($request, Closure $next)
    {
        $tenant = $request->route('tenant');
        abort_unless(Tenant::whereKey($tenant)->exists(), 404);

        config(["lighthouse.schemas.{$tenant}" => [
            ...config('lighthouse.schemas.tenant_template'),
            'uri'         => "/graphql/{$tenant}",
            'schema_path' => base_path("graphql/tenants/{$tenant}.graphql"),
        ]]);

        return $next($request);
    }
}

Register a single parameterised route for this instead of one route per tenant — otherwise route caching and tenant provisioning both become painful. Never let the schema name come straight from the URL into a file path without an existence check.

Resolver reuse with policy-level divergence. Put one PostResolver behind both schemas; let @can(ability: "view", find: "id") and per-schema policy registration do the difference. This keeps the admin/public field contracts aligned and makes permission bugs visible in one place.

Programmatic execution for internal calls. For server-side composition (SSR preloading, webhook fan-out), dispatch through Lighthouse's schema builder rather than issuing an HTTP request:

$result = GraphQL::executeQuery(
    Lighthouse::schema('admin')->buildSchema(),
    $query,
    rootValue: null,
    contextValue: ['request' => $request],
);

Testing. Each endpoint is a distinct $this->postJson('/admin/graphql', ...); assert on errors[].extensions.category (Lighthouse emits validation, authentication, authorization) rather than message strings, since the schema split means the same resolver surfaces under different auth contexts.

Gotchas and Tips

  • Route/config caching vs. runtime schemas. If registration happens in a service provider boot(), php artisan route:cache and config:cache will freeze the schema list. Tenant-driven setups must use one catch-all route plus request middleware; static setups should register everything in config so caching stays valid.
  • Middleware merge semantics. Lighthouse merges global middleware with the schema's middleware array. Leaving out AcceptJson/AddHeaders on a new schema produces content-type and CORS surprises that look like GraphQL bugs.
  • Guard leakage. @auth defaults to the app's default guard regardless of the schema's guard key unless you use @guard(with: ...) or AttemptAuthentication per schema. An admin schema on the sanctum guard will silently accept a cookie-authenticated web session otherwise.
  • Directive registration is app-global. Custom directives registered in lighthouse.php are available to every schema. Per-schema directive restriction is not automatic — if you need it, restrict by wrapping the directive's handleField in a schema-name check, or accept that exposure and secure via field-level @can.
  • Security limits are global. lighthouse.security.max_query_complexity, max_query_depth, and disable_introspection are read once for the app. A stricter budget for the public schema needs schema-specific middleware or a custom QueryComplexity invocation rather than config alone.
  • Cache-key collisions. @cache keys derive from field args, not the schema name. If both schemas resolve the same Post id with different visibility rules, they will share a cache entry. Namespace per schema, either via a custom @cacheKey or by prefixing args.
  • Schema validation per file. php artisan lighthouse:validate-schema validates one schema at a time in most versions; wire it into CI once per schema (a loop over your config keys) or you will only ever validate the default.
  • IDE/Playground. Lighthouse's GraphQL Playground route points at the default schema. Multi-schema setups usually need a per-schema IDE route or a URI switcher; check the package's route registration before assuming the IDE "just works" for the admin schema.
  • Error formatting and logging. Lighthouse\Execution\ErrorHandler is a single binding. If you want public schemas to hide internal messages while admin schemas expose them, override the handler and branch on the resolved schema name from the container — not on the URI string.
  • Subscriptions. The broadcasting auth endpoint and Pusher channel conventions are configured once; if only one schema uses subscriptions, the other schema's clients will still be able to hit that auth route unless you gate it.
  • Debugging tip. Log the resolved schema name in a terminating middleware alongside the route name — 90% of "wrong resolver ran" reports in multi-schema apps trace back to a schema name resolving to the default rather than the intended one, often because middleware ran after the GraphQL controller resolved the schema.
  • Small ecosystem. With a low star count and no dependents, pin the version and read the package source for the schema registry class before relying on its behaviour; the Lighthouse schemas config is the upstream contract and the likely safe fallback if the package's abstraction drifts.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource