In addition the usual CRUD functionality, Backpack also allows you to do a few more complicated things:
Starting with Backpack\CRUD 4.0, routes are defined inside the Controller, in methods that look like setupOperationNameRoutes()
; you can use this naming convention to setup extra routes, for your custom operations:
protected function setupModerateRoutes($segment, $routeName, $controller) {
Route::get($segment.'/{id}/moderate', [
'as' => $routeName.'.moderate',
'uses' => $controller.'@moderate',
'operation' => 'moderate',
]);
Route::post($segment.'/{id}/moderate', [
'as' => $routeName.'.saveModeration',
'uses' => $controller.'@saveModeration',
'operation' => 'moderate',
]);
}
If you want the route to point to a different controller, you can add the route in routes/backpack/custom.php
instead.
Backpack loads its views through a double-fallback mechanism:
resources/views/vendor/backpack/crud
folder, it will pick up those instead; you can use this method to overwrite a blade file for all CRUDs;setup()
method, to change a particular view:
$this->crud->setShowView('your-view');
$this->crud->setEditView('your-view');
$this->crud->setCreateView('your-view');
$this->crud->setListView('your-view');
$this->crud->setReorderView('your-view');
$this->crud->setDetailsRowView('your-view');
If you want to add extra CSS or JS to a certain page, use the script
and style
widgets to add a new file of that type onpage, either from your CrudController or a custom blade file:
use Backpack\CRUD\app\Library\Widget;
// script widget - works the same for both local paths and CDN
Widget::add()->type('script')->content('assets/js/custom-script.js');
Widget::add()->type('script')->content('https://code.jquery.com/ui/1.12.0/jquery-ui.min.js');
Widget::add()->type('script')
->content('https://code.jquery.com/ui/1.12.0/jquery-ui.min.js')
->integrity('sha256-0YPKAwZP7Mp3ALMRVB2i8GXeEndvCq3eSl/WsAl1Ryk=')
->crossorigin('anonymous');
// style widget - works the same for both local paths and CDN
Widget::add()->type('style')->content('assets/css/custom-style.css');
Widget::add()->type('style')->content('https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/dist/themes/light.css');
Widget::add()->type('style')
->content('https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/dist/themes/light.css')
->integrity('sha256-0YPKAwZP7Mp3ALMRVB2i8GXeEndvCq3eSl/WsAl1Ryk=')
->crossorigin('anonymous');
For more details please see the script
and style
sections in the Widgets page.
You can limit where that CSS/JS is added by making the Widget::add()
call in the right place in your CrudController:
Widget::add()
inside the setupListOperation()
method, it will only be loaded there;Widget::add()
inside the setup()
method, it will be loaded on all pages for that CRUD;CustomCrudController
;config/backpack/base.php
, under scripts
and styles
;If you try to add multiple columns with the same name
, by default Backpack will only show the last one. That's because name
is also used as a key in the $column
array. So when you addColumn()
with the same name twice, it just overwrites the previous one.
In order to insert two columns with the same name, use the key
attribute on the second column (or both columns). If this attribute is present for a column, Backpack will use key
instead of name
. Example:
$this->crud->addColumn([
'label' => "Location",
'type' => 'select',
'name' => 'location_id',
'entity' => 'location',
'attribute' => 'title',
'model' => "App\Models\Location"
]);
$this->crud->addColumn([
'label' => "Location Type",
'type' => 'radio_location_type',
'options' => [
1 => "Country",
2 => "Region"
],
'name' => 'location_id',
+ 'key' => 'location_type',
'entity' => 'location',
'attribute' => 'type',
'model' => "App\Models\Location"
]);
All Backpack packages allow you to easily overwrite and customize the views. If you want Backpack to pick up your file, instead of the one in the package, you can do that by just placing a file with the same name in your views. So if you want to overwrite the select2 field (vendor/backpack/crud/src/resources/views/fields/select2.blade.php
) to change some functionality, you need to create resources/views/vendor/backpack/crud/fields/select2.blade.php
. You can do that manually, or use this command:
php artisan backpack:field --from=select2
This will look for the blade file and copy it inside the folder, so you can edit it.
Please note: Once you place a file with the same name inside your project, Backpack will pick that one up instead of the one in the package. That means that even though the file in the package is updated, you won't be getting those updates, since you're not using that file. Blade modifications are almost never breaking changes, but it's a good thing to receive updates with zero effort. Even small ones. So please overwrite the files as little as possible. Best to create your own custom fields/column/filters/buttons, whenever you can.
This also applies to: select2, select2_multiple, select2_from_ajax, select2_from_ajax_multiple.
There are 3 possible solutions:
select_from_array
or select2_from_array
instead. Since you tell it what the options are, you can do any filtering you want there.select2_from_ajax
instead. You'll be able to filter the results any way you want in the controller method (the one that responds with the results to the AJAX call).select2_from_array
for select2_from_ajax
, you can create another model and add a global scope to it. For example, say you only want to show the users that belong to the user's company. You can create App\Models\CompanyUser
and use that in your select2
field instead of App\Models\User
:<?php
namespace App\Models;
use App\User;
use Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class CompanyUser extends User
{
/**
* The "booting" method of the model.
*
* @return void
*/
protected static function boot()
{
parent::boot();
// never let a company user see the users of other companies
if (Auth::check() && Auth::user()->company_id) {
$companyId = Auth::user()->company->id;
static::addGlobalScope('company_id', function (Builder $builder) use ($companyId) {
$builder->where('company_id', $companyId);
});
}
}
}
If you're developing a package, you might need Backpack to pick up fields from your package folder, instead of having to publish them upon installation.
Fields, Columns and Filters all have a view_namespace
parameter you can use. Type your folder there, and Backpack will check that folder first, then where the views are published, then Backpack's package folder. Example:
$this->crud->addFilter([ // add a "simple" filter called Draft
'type' => 'complex',
'name' => 'checkbox',
'label' => 'Checked',
'view_namespace' => 'custom_filters'
],
false, // the simple filter has no values, just the "Draft" label specified above
function () { // if the filter is active (the GET parameter "draft" exits)
$this->crud->addClause('where', 'checkbox', '1');
});
This will make Backpack look for the resources/views/custom_filters/complex.blade.php
, and pick that up before anything else.
The relationship
, select2_from_ajax
and select2_from_ajax_multiple
fields allow you to filter the results depending on what has already been written or selected in a form. Say you have two select2
fields, when the AJAX call is made to the second field, all other variables in the page also get passed - that means you can filter the results of the second select2
.
Say you want to show two selects:
// select2
$this->crud->addField([
'label' => 'Category',
'type' => 'select',
'name' => 'category',
'entity' => 'category',
'attribute' => 'name',
]);
// select2_from_ajax: 1-n relationship
$this->crud->addField([
'label' => "Article", // Table column heading
'type' => 'select2_from_ajax_multiple',
'name' => 'articles', // the column that contains the ID of that connected entity;
'entity' => 'article', // the method that defines the relationship in your Model
'attribute' => 'title', // foreign key attribute that is shown to user
'data_source' => url('api/article'), // url to controller search function (with /{id} should return model)
'placeholder' => 'Select an article', // placeholder for the select
'include_all_form_fields' => true, //sends the other form fields along with the request so it can be filtered.
'minimum_input_length' => 0, // minimum characters to type before querying results
'dependencies' => ['category'], // when a dependency changes, this select2 is reset to null
// 'method' => 'POST', // optional - HTTP method to use for the AJAX call (GET, POST)
]);
DIFFERENT HERE: minimum_input_length
, dependencies
and include_all_form_fields
.
Note: if you are going to use include_all_form_fields
we recommend you to set the method to POST
, and to properly setup that in your routes. Since all the fields in the form are going to be sent in the request, POST
support more data.
Route::post('api/article', 'App\Http\Controllers\Api\ArticleController@index');
Route::post('api/article/{id}', 'App\Http\Controllers\Api\ArticleController@show');
DIFFERENT HERE: Nothing.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Backpack\NewsCRUD\app\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function index(Request $request)
{
$search_term = $request->input('q'); // the search term in the select2 input
// if you are inside a repeatable we will send some aditional data to help you
$triggeredBy = $request->input('triggeredBy'); // you will have the `fieldName` and the `rowNumber` of the element that triggered the ajax
// NOTE: this is a Backpack helper that parses your form input into an usable array.
// you still have the original request as `request('form')`
$form = backpack_form_input();
$options = Article::query();
// if no category has been selected, show no options
if (! $form['category']) {
return [];
}
// if a category has been selected, only show articles in that category
if ($form['category']) {
$options = $options->where('category_id', $form['category']);
}
if ($search_term) {
$results = $options->where('title', 'LIKE', '%'.$search_term.'%')->paginate(10);
} else {
$results = $options->paginate(10);
}
return $results;
}
public function show($id)
{
return Article::find($id);
}
}
DIFFERENT HERE: We use $form
to determine what other variables have been selected in the form, and modify the result accordingly.
With so many field types, it can be a little overwhelming to understand what field type to use for a particular Eloquent relationship. Here's a quick summary of all possible relationships, and the interface you might want for them. Click on the relationship you're interested in, for more details and an example:
relationship
field with subfields
passport.title
)relationship
fieldrelationship
fieldrelationship
field with subfields
relationship
fieldrelationship
field and define subfields
relationship
field with subfields
passport.title
)relationship
fieldrelationship
field with subfields
relationship
fieldrelationship
field and define subfields
_type
and _id
of the morphTo relation;User -> hasOne -> Phone
user_id
on phones
table)relationship
field with subfields
defined for each column on the related entryhasOne
relationship should be properly defined in the User model;// inside UserCrudController::setupCreateOperation()
CRUD::field('phone')->type('relationship')->subfields([
'prefix',
'number',
[
'name' => 'type',
'type' => 'select_from_array',
'options' => ['mobile' => 'Mobile Phone', 'landline' => 'Landline', 'fax' => 'Fax'],
]
]);
User -> hasOne -> Phone
user_id
on phones
table)text
, number
, textarea
), with the field name prefixed by the relationship name (dot notation);hasOne
relationship should be properly defined in the User model;relationship_name.column_name
); note that the prefix (before the dot) is the Relation name, not the table name;text
field, number
field, textarea
field, select
field etc.;// inside UserCrudController::setupCreateOperation()
CRUD::field('phone.number')->type('number');
CRUD::field('phone.prefix')->type('text');
CRUD::field('phone.type')->type('select_from_array')->options(['mobile' => 'Mobile Phone', 'landline' => 'Landline', 'fax' => 'Fax']);
Phone -> User
user_id
on phones
table)belongsTo
relationship should be properly defined in the Phone model;relationship
or select
field;relationship
or select2
field;relationship
field and Fetch operation (alternatively, use the select2_from_ajax
field);// inside PhoneCrudController::setupCreateOperation()
CRUD::field('user'); // notice the name is the relationship name and backpack will auto-infer the field type as [`relationship`](#relationship)
CRUD::field('user_id')->type('select')->model('App\Models\User')->attribute('name')->entity('user'); // notice the name is the foreign key attribute
CRUD::field('user_id')->type('select2')->model('App\Models\User')->attribute('name')->entity('user'); // notice the name is the foreign key attribute
relationship
field, you could also use the InlineCreate operation, which will add a [+ Add Item] button next to the dropdown, to let the admin create a User in a modal, without leaving the current Create Phone form;Post -> HasMany -> Comment
post_id
on comments
table)relationship
field;hasMany
relationship should be properly defined in the Post model;// inside PostCrudController::setupCreateOperation()
CRUD::field('comments'); // when unselected, will set foreign key to null
CRUD::field('comments')->fallback_id(3); // when unselected, will set foreign key to 3
CRUD::field('comments')->force_delete(true); // when unselected, will delete the related entry
null
, if that db column is nullable (eg. post_id
);fallback_id
on the field;'force_delete' => false
on the field;post_id
on comments table need to nullable or have a default setup in database;If you want the admin to not only select an entry, but also create them, edit their attributes or delete related entries.
Post -> HasMany -> Comment
post_id
on comments
table)relationship
field;hasMany
relationship should be properly defined in the Post model;// inside PostCrudController::setupCreateOperation()
CRUD::field('comments')->subfields([['name' => 'body']]);
// where body is a text field in the comment table.
Note: Starting with v5, the BelongsToMany
relation had been improved to simplify the scenario where your pivot table has extra database columns (in addition to the foreign keys).
User -> BelongsToMany -> Role
user_roles
table has both user_id
and role_id
)belongsToMany
relationship should be properly defined in both the User and Role models;relationship
, select_multiple
, select2_multiple
or select2_from_ajax_multiple
fields;// inside UserCrudController::setupCreateOperation()
CRUD::field('roles');
// inside RoleCrudController::setupCreateOperation()
CRUD::field('users');
relationship
field type, you can also use the InlineCreate operation, which will add a [+ Add Item]
button next to the dropdown, to let the admin create a User/Role in a modal, without leaving the current Create User/Role form;If your pivot table has additional database columns (eg. not only user_id
and role_id
but also notes
or supervisor
, you can use the relationship
field to show a subform, instead of a select2
, and show subfields
for each of those attributes you want edited on the pivot table. For the example above (User -> BelongsToMany -> Roles
) you should do the following:
Step 1. Setup the pivot fields in your relation definition:
// inside App\Models\User
public function roles() {
return $this->belongsToMany('App\Models\Role')->withPivot('notes', 'some_other_field'); // `notes` and `some_other_field` are aditional fields in the pivot table that you plan to show in the form.
}
Step 2. Setup the pivot fields in your relation definition:
// inside UserCrudController::setupCreateOperation()
CRUD::field('roles')->subfields([
['name' => 'notes', 'type' => 'textarea'],
['name' => 'some_other_field']
]);
And you are done: a subform will shown, with a select for the pivot connected entity field and the defined fields and Backpack will take care of the saving process.
Need to change the pivot select
field? You can add any configuration to the pivot field as you would do in a relationship select field, the only difference is is that it should go inside the pivotSelect
key:
CRUD::field('users')->subfields([ ['name' => 'notes'] ])
->pivotSelect([
'ajax' => true,
'data_source' => backpack_url('role/fetch/user'),
'placeholder' => 'some placeholder',
'wrapper' => [
'class' => 'col-md-6'
]
]);
morphOne
relationship should be properly defined in both the Post/User and Video models;You can add a subform for the related entry to be created/edited/deleted from the main form:
CRUD::field('video')->type('relationship')->subfields([
'url',
[
'name' => 'description',
'type' => 'ckeditor',
]
]);
Backpack will take care of the saving process and deal with the morphable relation.
morphOne
relationship should be properly defined in both the Post/User and Video models;You can add any type of field to change the attribute on the related entry, but make sure to prefix the field name with the name of the relationship:
CRUD::field('video.description')->type('ckeditor');
CRUD::field('video.url');
Backpack will take care of the saving process and deal with the morphable relation.
This is in all aspects similar to HasMany relation, the difference is that it's stored in a pivot table.
morphMany
relationship should be properly defined in both the Post/Video and Comment models;There is no sense in using this a select
when using a polymorphic relation because the items that could/would be select might belong to different entities. So you should setup this relation as you would setup a HasMany creatable.
// inside PostCrudController::setupCreateOperation() and inside VideoCrudController::setupCreateOperation()
CRUD::field('comments')->subfields([['name' => 'comment_text']]); //note comment_text is a text field in the comment table.
This is in all aspects similar to BelongsToMany relation, the difference is that it stores the morphable
entity in the pivot table:
morphToMany
relationship should be properly defined in both the Post/Video and Tag models;Please read the relationship BelongsToMany documentation, everything is the same in regards to fields definition and Backpack will take care of the morphable relation saving.
Using this relation type Backpack will automatically manage for you both _type
and _id
fields of this relation.
Let's say we have comments
, that can be either for videos
or posts
.
Comment
Model should have its morphTo
relation set up.commentable_type
and commentable_id
columns.
// in CommentCrudController you can add the morphTo fields by naming the field the morphTo relation name
CRUD::field('commentable')
->addMorphOption('App\Models\Video');
->addMorphOption('App\Models\Post');
This will generate two inputs:
1 - A select with two options Video
and Post
as the morph type field
.
2 - A second select that will have the options for both Video
and Post
models.
In a real world scenario, you might have other needs, like using AJAX to select the actual entries or changing the inputs size etc. For that, check out the available attributes:
// ->addMorphOption(string $model/$morphMapName, string $labelInSelect, array $options)
CRUD::field('commentable')
->addMorphOption('App\Models\Video')
->addMorphOption('App\Models\Post', 'Posts', [
[
'data_source' => backpack_url('comment/fetch/post'),
'minimum_input_length' => 2,
'placeholder' => 'select an amazing post',
'method' => 'POST',
'attribute' => 'title',
]
]);
// by defining `data_source` you are telling Backpack that the `Posts` select should be an ajax select.
In this scenario the same two selects would be generated, but for the Post, your admin see an AJAX field, instead of a static one, use POST instead of GET etc.
To further customize the fields you can use morphTypeField
and morphIdField
to configure the select sizes etc.
CRUD::field('commentable')
->addMorphOption('App\Models\Video')
->addMorphOption('App\Models\Post', 'Posts')
->morphTypeField(['wrapper' => ['class' => 'form-group col-sm-4']])
->morphIdField(['wrapper' => [
'class' => 'form-group col-sm-8'],
'attributes' => ['my_custom_attribute' => 'custom_value']
]);
Here is an example using array field definition:
$this->crud->addField([
'name' => 'commentable',
'morphOptions' => [
['App\Models\PetShop\Owner', 'Owners'],
['monster', 'Monsters', [
'placeholder' => 'Select a little monster'
]],
['App\Models\PetShop\Pet', 'Pets', [
'data_source' => backpack_url('pet-shop/comment/fetch/pets'),
'minimum_input_length' => 2,
'placeholder' => 'select a fluffy pet'
]],
],
'morphTypeField' => [
'wrapper' => ['class' => 'form-group col-md-6']
],
'morphIdField' => [
'wrapper' => ['class' => 'form-group col-md-6']
]
]);
latestOfMany()
or oldestOfMany()
).latestOfMany()
or oldestOfMany()
).You might want to add a new attribute to the Model that gets saved. Let's say you want to add an updated_by
indicator to the Update operation, containing the ID of the user currently logged in (backpack_user()->id
).
By default, Backpack it will only save inputs that have a corresponding CRUD field defined. But you can override this behaviour, by using the setting called strippedRequest
, which determine the which fields should actually be saved, and which fields should be "stripped" from the request.
Here's how you can use strippedRequest
to add an updated_by
item to be saved (but this will work for any changes you want to make to the request, really). You can change the request at various points in the request:
CRUD::setOperationSetting('strippedRequest', StripBackpackRequest::class);
in your setup()
);prepareForValidation()
);config/backpack/operations/update.php
);Let's demonstrate each one of the above:
Option 1. In the controller. You can change the strippedRequest
closure inside your ProductCrudController::setup()
:
public function setupUpdateOperation()
{
CRUD::setOperationSetting('strippedRequest', function ($request) {
// keep the recommended Backpack stripping (remove anything that doesn't have a field)
// but add 'updated_by' too
$input = $request->only(CRUD::getAllFieldNames());
$input['updated_by'] = backpack_user()->id;
return $input;
});
}
Option 2. In the request. You can change the same strippedRequest
closure inside the ProductFormRequest
that contains your validation:
protected function prepareForValidation()
{
\CRUD::set('update.strippedRequest', function ($request) { //notice here that update is refering to update operation, change accordingly
// keep the recommended Backpack stripping (remove anything that doesn't have a field)
// but add 'updated_by' too
$input = $request->only(\CRUD::getAllFieldNames());
$input['updated_by'] = backpack_user()->id;
return $input;
});
}
Option 3. In the config file. You cannot use a closure (because closures don't get cached). But you can create an invokable class, and use that as your strippedRequest
, in your config/backpack/operations/update.php
(for example). Then it will apply to ALL update operations, on all entities:
<?php
namespace App\Http\Requests;
use Illuminate\Http\Request;
class StripBackpackRequest
{
public function __invoke(Request $request)
{
$input = $request->only(\CRUD::getAllFieldNames());
$input['updated_by'] = backpack_user()->id;
return $input;
}
}
In practice, what you want is to change the class on the main <div>
of the Create/Update operation. To learn how to do that, please take a look at the next section - how to make an operation wider or narrower.
If you want to make the contents of an operation take more / less space from the window, you can easily do that. You just need to change the class on the main <div>
of that operation, what we call the "content class". Depending on the scope of your change (for one or all CRUDs) here's how you can do that:
(A) for all CRUDs, by specifying the custom content class in your config/backpack/crud.php
:
// Here you may override the css-classes for the content section of the create view globally
// To override per view use $this->crud->setCreateContentClass('class-string')
'create_content_class' => 'col-md-8 col-md-offset-2',
// Here you may override the css-classes for the content section of the edit view globally
// To override per view use $this->crud->setEditContentClass('class-string')
'edit_content_class' => 'col-md-8 col-md-offset-2',
// Here you may override the css-classes for the content section of the revisions timeline view globally
// To override per view use $this->crud->setRevisionsTimelineContentClass('class-string')
'revisions_timeline_content_class' => 'col-md-10 col-md-offset-1',
// Here you may override the css-class for the content section of the list view globally
// To override per view use $this->crud->setListContentClass('class-string')
'list_content_class' => 'col-md-12',
// Here you may override the css-classes for the content section of the show view globally
// To override per view use $this->crud->setShowContentClass('class-string')
'show_content_class' => 'col-md-8 col-md-offset-2',
// Here you may override the css-classes for the content section of the reorder view globally
// To override per view use $this->crud->setReorderContentClass('class-string')
'reorder_content_class' => 'col-md-8 col-md-offset-2',
(B) for a single CRUD, by using:
$this->crud->setCreateContentClass('col-md-8 col-md-offset-2');
$this->crud->setUpdateContentClass('col-md-8 col-md-offset-2');
$this->crud->setListContentClass('col-md-8 col-md-offset-2');
$this->crud->setShowContentClass('col-md-8 col-md-offset-2');
$this->crud->setReorderContentClass('col-md-8 col-md-offset-2');
$this->crud->setRevisionsTimelineContentClass('col-md-8 col-md-offset-2');
The default Backpack installation doesn't come with a file management component. Because most projects don't need it. But we've created a first-party add-on that brings the power of elFinder to your Laravel projects. To install it, follow the instructions on the add-ons page. It's as easy as running:
# require the package
composer require backpack/filemanager
# then run the installation process
php artisan backpack:filemanager:install
If you've chosen to install backpack/filemanager, you'll have elFinder integrated into:
For the integration, we use barryvdh/laravel-elfinder.
If the automatic installation doesn't work for you and you need to manually install CRUD, here are all the commands it is running:
1) In your terminal:
composer require backpack/crud
2) Instead of running php artisan backpack:install
you can run:
php artisan vendor:publish --provider="Backpack\CRUD\BackpackServiceProvider" --tag="minimum"
php artisan migrate
php artisan backpack:publish-middleware
Starting with Backpack v4, you can use a custom CrudPanel object instead of the one in the package. In your custom CrudPanel object, you can overwrite any method you want, but please note that this means that you're overwriting core components, and will be making it more difficult to upgrade to newer versions of Backpack.
You can do this in any of your service providers (ex: app/Providers/AppServiceProvider.php
) to load your class instead of the one in the package:
$this->app->extend('crud', function () {
return new \App\MyExtendedCrudPanel;
});
Details and implementation here.
When trying to install Backpack\PRO (or any of our closed-source add-ons, really), you might run into the following error message:
Downloading backpack/pro (1.1.1)
Failed to download backpack/pro from dist: The "https://backpackforlaravel.com/satis/download/dist/backpack/pro/backpack-pro-xxx-zip-zzz.zip" file could not be downloaded (HTTP/2 402 )
Or maybe:
Syncing backpack/pro (1.1.1) into cache
Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos
Head to https://github.com/settings/tokens/new?scopes=repo&description=Composer+on+DESKTOP-BLABLA+2022-07-14+1559
to retrieve a token.
What's happening there? That is a general Composer error - "file could not be downloaded". The error itself doesn't give too much information, but we can make an educated guess.
99% of the people who report this error have the same problem - they do not have access to that package version. They bought updates until 1.0.13 (for example), so they DO NOT have access to the latest version (1.1.1 in this example). What you can do, in that case, is lock the installation to the latest you have access to, for example
composer require backpack/pro:"1.0.13"
Alternatively, you can purchase more access on the Backpack website. Or contact the team if there's a mistake.
--
How do you find out what's the last version you have access to?
(1) Whenever the error above happens, Backpack will send you an email, with details and instructions. Check your email, it will also include the latest version you have access to.
(2) Your Tokens page will show more details. For each token you have, it will say when it stops giving you access to updates. If it doesn't say the last version directly, you can corroborate that last day with the changelog, to determine what's the last version that you have access to.
--
Why the ugly, general error? Because Composer doesn't allow vendors to customize the error, unfortunately. Backpack's server returns a better error message, but Composer doesn't show it.
Then you'll love our premium add-ons - productivity tools and tons of new features.