Guide 07 · Developer docs

Members, accounts and gated content

Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.

L2
Unit 01 · The identity model

Person, user and author

A person is the human and their contact details, a user is the login, and an author is an editorial byline. Keeping the three apart from the first day avoids data that is expensive to move later. What each holds, how a member and an editor are made of them, and the current user in a template.

Who this is for. Everyone starting on a member site: the theme developer, the content modeller and the integrations engineer. Afterwards you know which record a piece of member data belongs on, and how to read the signed-in visitor in a template.

Three records

RecordIsHolds
PersonThe humanEmail address, title, first, middle and last name, gender, addresses, groups, custom fields
UserThe login, belonging to one personUsername, password (or none, for someone who signs in only through a provider or a passkey), type, status, custom fields, and the person's API keys, tokens and sign-in methods
AuthorA public byline, belonging to one personDisplay name, biography, website, social links, picture. Blogs and blog posts use it

A person can exist without a user: a newsletter subscriber, someone who filled in a form, a customer imported from another system. A person has at most one user and at most one author. The author has nothing to do with signing in: being a byline neither needs nor grants a login.

A member and an editor, side by side

A memberAn editor who writes for the blog
PersonYes: name, email address, addressesYes: name, email address
UserYes, of type Frontend user: signs in on the websiteYes, of a backend type such as Administrator: signs in to the admin
AuthorNoYes: the name and picture shown on their posts

The user's type is what makes one a member and the other an administrator; the records are otherwise the same. Members and backend users covers the types and what keeps members out of the admin.

Where member data belongs

  • Contact details go on the person, never copied onto the user or the author. The email address is the person's.
  • The email address is unique among all the people of the account: a second person with the same address is refused with "This email is already used." People and users belong to the account, not to one site, so a client with several sites on one account has one set of them.
  • Data the member owns, such as a phone number or preferences, goes in custom fields on the person; data that repeats or has its own life goes in related records (Profile data: custom fields or a related record).
  • Addresses are the person's address book (Addresses and the address book).
  • What the member may see comes from groups and permissions (Groups and permissions), never from a value stored on the member's own records.

An integration that imports customers creates people; it creates users only for the customers who are to sign in. A form submission can create a person too, and that person still cannot sign in (Create a person record from a form submission).

The current user in a template

Every template receives _currentUser, a small helper describing who is looking. It is not the user record:

MethodReturns
isLoggedIn()Whether anyone is signed in
getId(), getPersonId()The user's id and their person's id
getUsername(), getFullName(), getType()As the names say; the type is frontend, backend, admin, superadmin or developer
isFrontendUser(), isBackendUser()Whether the type is frontend, or anything else
isAdmin(), isSuperAdmin(), isDeveloper()True for administrators, super admins and developers; super admins and developers; developers
getPerson()The person record, or nothing when nobody is signed in

Two details catch people out. isFrontendUser() is also true for a signed-out visitor, whose type defaults to frontend, so test isLoggedIn() first. And getPerson() is empty for a signed-out visitor, so calling a method on it without the test stops the page.

Template code
{if $_currentUser->isLoggedIn()}
  {$person = $_currentUser->getPerson()}
  <p>Signed in as {$person->getFirstName()|escape} ({$person->getEmail()|escape})</p>
{else}
  <p><a href="{$_url->getPath('login')}">Sign in</a></p>
{/if}
{% if _currentUser.isLoggedIn() %}
  {% set person = _currentUser.getPerson() %}
  <p>Signed in as {{ person.getFirstName()|e }} ({{ person.getEmail()|e }})</p>
{% else %}
  <p><a href="{{ _url.getPath('login') }}">Sign in</a></p>
{% endif %}

Neither engine escapes output on this platform, so escape anything a member typed. And a template that prints who is looking must not be stored in the page cache: on a public page, put that part in a fragment (Member pages and the page cache). Pages under /my-account also receive user, the full user record (What your template receives).

Go deeper

Was this page helpful?

Your answer goes to the docs team.