Configuration
Configure Lighthouse from a single initializer. Keeping app-specific wiring here means the library holds no app constants — which is what makes it cleanly reusable and publishable.
# config/initializers/lighthouse.rb
require 'lighthouse-graphql'
Lighthouse.configure do |config|
config.schema_path = Rails.root.join('graphql/schema.graphql').to_s
config.namespaces = { queries: ['Resolvers'], mutations: ['Mutations'] }
config.resolver_namespaces = ['App::Graphql']
config.on_error = ->(error, _context) { Rails.error.report(error) }
end
Options
schema_path
The schema's entrypoint, as PHP Lighthouse's schema_path. Lighthouse reads this file and the files it imports (#import user.graphql, #import blog/*.graphql). A type is defined once, and other files add to it with extend type. See schema-organisation.md. Defaults to RAILS_ROOT/graphql/schema.graphql in a Rails app, and nil otherwise. Outside Rails, pass the path, or the SDL itself (SchemaFactory.build_base_schema(sdl:)).
get_schema(schema_path: ...) overrides it for one schema.
ide_helper
Rewrite schema-directives.graphql and programmatic-types.graphql in the app's root on boot and on every reload, for the IDE (schema-organisation.md). A file whose content is unchanged is left alone. nil (the default) turns it on in development when the app has a graphql.config.yml, which lighthouse:install writes. Set true or false to decide yourself.
namespaces
Where each kind of class a schema names is looked up, by kind — they have different contracts, so they live apart (resolvers.md):
| Kind | Default | Holds |
|---|---|---|
queries | ['Resolvers'] | Resolvers::BaseResolver classes (a GraphQL::Schema::Resolver) for Query fields. ['Queries'] before 0.4.0 |
mutations | ['Mutations'] | Mutations::BaseMutation classes (a GraphQL::Schema::Mutation) for Mutation fields |
builders | ['Builders'] | @builder classes |
validators | ['Validators'] | @validator contracts |
events | ['Events'] | Classes @event(dispatch:) names (events.md) |
subscriptions | ['Subscriptions'] | Subscriptions::BaseSubscription classes (a GraphQL::Schema::Subscription; the base includes Lighthouse::GraphQL::Subscription) for Subscription fields, and @subscription(class:) (subscriptions.md) |
analyzers | ['Analyzers'] | GraphQL::Analysis::Analyzer classes (optionally including Lighthouse::GraphQL::ComplexityAnalyzer) that @complexity(resolver:) names (security.md) |
types | ['Types'] | Types::<Name>Type classes including Lighthouse::GraphQL::Type: an object type's, whose methods resolve the fields of the SDL type <Name> (resolvers.md); a union's or interface's, which picks its values' types; a scalar's, which coerces its values (types.md). Also where @union, @interface and @scalar look classes up |
config.namespaces = { queries: ['Api::Resolvers'] } # the other kinds keep their defaults
resolver_namespaces
An ordered list of namespaces tried for models named by a directive (@all(model: "User")) and for @field(resolver:) on types other than Query and Mutation, after the resolvers namespace (resolvers.md). Fully-qualified names (containing ::) are used as-is. Defaults to ['App::Graphql'].
config.resolver_namespaces = ['App::Graphql', 'Resolvers']
# "ContactPerson" tries App::Graphql::ContactPerson, then Resolvers::ContactPerson, then ::ContactPerson
This is the seam that keeps the gem free of your app's namespace.
on_error
A callable ->(error, context) { ... } invoked when a directive or resolver swallows an error (e.g. a malformed filter). The default raises in test (so failures surface in your suite) and logs otherwise. Point it at your error reporter in production:
config.on_error = ->(error, _ctx) { Sentry.capture_exception(error) }
pagination_default_count, pagination_max_count
Defaults for paginated fields: the first used when a client leaves it out (so it is no longer required), and the largest first allowed. A directive's defaultCount / maxCount override them; maxCount: null lifts the cap.
config.pagination_default_count = 25
config.pagination_max_count = 100
schema_cache, schema_cache_path, watch_sdl, persisted_queries*
Options for large schemas and high traffic, all off by default. See performance.md.
config.schema_cache = Rails.env.production? # cache the built schema in tmp/cache
config.watch_sdl = Rails.env.development? # reload when a .graphql file changes
config.persisted_queries = true # Apollo's Automatic Persisted Queries
config.persisted_queries_store = Rails.cache # where APQ keeps queries (default: Rails.cache)
config.persisted_queries_ttl = 1.week.to_i # how long they live there
config.persisted_queries_manifest = Rails.root.join('persisted-queries.json') # trusted operations
config.persisted_queries_only = Rails.env.production? # run nothing else
convert_empty_strings_to_null
Makes every field behave as if it carried @convertEmptyStringsToNull: empty strings sent for nullable inputs arrive as nil, non-null inputs unchanged. Defaults to false. It is read when the schema is built, so set it in the initializer.
config.convert_empty_strings_to_null = true
global_id
Encodes and decodes Relay global ids for @globalId and @node: any object with encode(type, id) returning the global id, and decode(global_id) returning [type, id]. Defaults to Lighthouse::GraphQL::GlobalId, the base64 of Type:id, as PHP Lighthouse's.
config.global_id = SignedGlobalIds
queue_broadcasts, broadcast_queue
Whether @broadcast triggers its subscription in an ActiveJob (Lighthouse::GraphQL::BroadcastJob, needs the activejob gem) instead of inside the mutation's request, and the queue it goes to. A directive's shouldQueue: overrides queue_broadcasts. Defaults: false, and nil (the default queue of ActiveJob). See subscriptions.md.
config.queue_broadcasts = true
config.broadcast_queue = :subscriptions
transactional_mutations
Whether @create / @update / @createMany / @updateMany run in a database transaction. Defaults to true.
soft_deletes
What "trashed" means, for @softDeletes, @trashed, @restore, @forceDelete and @withoutGlobalScopes. By default each model is inspected: paranoia (acts_as_paranoid), then discard (Discard::Model), then a deleted_at column. Set it for another column, or a scheme of your own:
# Another column, for models without a gem:
config.soft_deletes = Lighthouse::Support::SoftDeletes::Column.new(:archived_at)
# Any object answering these (relations in, relations out; records in, truthy out):
class SoftDeleteAdapter
def soft_deletes?(model) = model.include?(Archivable)
def only_trashed(relation) = relation.unscope(where: :archived_at).where.not(archived_at: nil)
def with_trashed(relation) = relation.unscope(where: :archived_at)
def without_trashed(relation) = relation.unscope(where: :archived_at).where(archived_at: nil)
def restore(record) = record.unarchive
def force_delete(record) = record.destroy
end
config.soft_deletes = SoftDeleteAdapter.new
See Soft deletes & global scopes.
authenticated_user, policy_user, authorizer
The authentication and authorization seams behind @guard and the @can* family. The authorizer is ->(user, record, ability, args = []), Pundit by default. See authorization.md.
feature_checker, feature_actor
The seams behind @feature. The checker is ->(name, actor, ctx) { true/false }, called with the feature's name as written in the SDL; by default it asks Flipper, Flipper.enabled?(name.to_sym, actor), and raises a clear error when Flipper is not loaded. The actor is ->(ctx) { actor }; by default it is the authenticated user (a Flipper actor answers flipper_id), and nil checks the feature globally.
Lighthouse.configure do |config|
config.feature_actor = ->(ctx) { ctx[:current_account] }
config.feature_checker = ->(name, account, _ctx) { account&.features&.include?(name) }
end
Extending the registry
Register app-defined directives (see custom-directives.md) from the same initializer:
Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::StartsWith)
Directives implemented inside another class — no class of their own to register — contribute their SDL definition with register_definition, so it is injected into the schema and written by bin/rails lighthouse:ide_helper:
Lighthouse::DirectiveRegistry.default.register_definition(
'tenant', 'directive @tenant(column: String) on FIELD_DEFINITION'
)
Types a definition refers to (an enum argument, say) are registered with register_type_definition('TenantMode', 'enum TenantMode { STRICT LOOSE }'), or returned by the directive class's type_definitions ({ name => sdl }). They are injected only alongside a directive that uses them, and only if the schema doesn't define them itself.
Federation reference resolvers
Custom federation entity resolvers are registered with Lighthouse::ReferenceResolver (see federation.md), typically in config/initializers/federation.rb so they re-register on code reload:
Rails.application.config.to_prepare do
Lighthouse::ReferenceResolver.register('User') { |ref, _ctx| User.find_by(id: ref['id']) }
end