diff --git a/.changeset/quick-stingrays-sing.md b/.changeset/quick-stingrays-sing.md
new file mode 100644
index 00000000000..76a494a41c9
--- /dev/null
+++ b/.changeset/quick-stingrays-sing.md
@@ -0,0 +1,5 @@
+---
+'@keystone-next/example-next-auth': major
+---
+
+Initial version of the `withAuth` example.
diff --git a/docs/components/Navigation.tsx b/docs/components/Navigation.tsx
index 90e395b3105..379db8fe769 100644
--- a/docs/components/Navigation.tsx
+++ b/docs/components/Navigation.tsx
@@ -65,6 +65,9 @@ export function Navigation() {
Getting started
+
+ Authentication
+
Keystone 5 vs Next
diff --git a/docs/pages/tutorials/with-auth.mdx b/docs/pages/tutorials/with-auth.mdx
new file mode 100644
index 00000000000..0ebdcecb296
--- /dev/null
+++ b/docs/pages/tutorials/with-auth.mdx
@@ -0,0 +1,112 @@
+import { Markdown } from '../../components/Page';
+
+Authentication
+
+In this tutorial we're going to demonstrate how to add password based authentication to your Keystone system.
+
+This tutorial builds on the [FIXME](./fixme) tutorials, so make sure you've completed that first before starting this one.
+If you want to jump straight in. you can grab the starting code from [FIXME](./FIXME).
+
+By the end of the tutorial you'll be able to add users with passwords to your task manager. and they'll be able to:
+
+ * Configures which fields to use for signin
+ * Sets up stateless session handling to keep track of the signed in user
+ * Adds a signin screen to the Admin UI
+ * Adds a signout button to the Admin UI
+ * Allows the signed in user to access their own details in the Admin UI
+ * Adds a helper page to the Admin UI to allow you to create your first user when starting from an empty database
+
+
+This project demonstrates how to add password based authentication to your Keystone system.
+It builds on the [todo](../) starter project.
+
+## Instructions
+
+To run this project, clone the Keystone repository locally then navigate to this directory and run:
+
+```shell
+yarn dev
+```
+
+This will start the Admin UI at [localhost:3000](http://localhost:3000).
+You can use the Admin UI to create items in your database.
+
+You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations.
+
+## Features
+
+This project shows you how to add authentication to your Keystone system. We're going to use the [`@keystone-next/auth`](https://next.keystonejs.com/apis/auth) package, along with Keystone's [session management API](https://next.keystonejs.com/apis/session), to add the following features to your system:
+
+ * Configures which fields to use for signin
+ * Sets up stateless session handling to keep track of the signed in user
+ * Adds a signin screen to the Admin UI
+ * Adds a signout button to the Admin UI
+ * Allows the signed in user to access their own details in the Admin UI
+ * Adds a helper page to the Admin UI to allow you to create your first user when starting from an empty database
+
+### Added fields
+
+We start by adding two new fields, `email` and `password`, to the `Person` list.
+These will be used as our _identity_ and _secret_ fields for login.
+
+```typescript
+ email: text({ isRequired: true, isUnique: true }),
+ password: password({ isRequired: true }),
+```
+
+### Auth config
+
+```typescript
+const { withAuth } = createAuth({
+ listKey: 'Person',
+ identityField: 'email',
+ secretField: 'password',
+ initFirstItem: { fields: ['name', 'email', 'password'] },
+});
+```
+
+### Session
+
+```typescript
+const session = statelessSessions({ secret: '-- EXAMPLE COOKIE SECRET; CHANGE ME --' });
+```
+
+### Wrapped config
+
+```typescript
+export default withAuth(
+ config({
+ db: {
+ provider: 'sqlite',
+ url: process.env.DATABASE_URL || 'file:./keystone-example.db',
+ },
+ lists,
+ session,
+ })
+);
+```
+
+## Screenshots
+
+### Admin UI
+
+Screenshots of the first item experience, mailing list experience(?), login screen, the "logged in as" bit, logout button.
+![initial user screen](../../screenshots/init-user-01.png)
+![mailing list subscription screen](../../screenshots/mailing-list-01.png)
+![sign in screen](../../screenshots/sign-in-screen-01.png)
+
+### GraphQL Playground
+
+Show off the new API that this has added and how to use it
+
+### Somehow show session cookie data.
+
+## Next steps
+
+This project is a bare bones system, and doesn't use any of Keystone's advanced features.
+We encourage you to experiment with the code here to see how Keystone works, become familiar with the Admin UI, and learn about the GraphQL Playground.
+
+Once you've got the hang of using this project, you can check out the [feature examples](../).
+These projects build on this starter project and show you how to use Keystones advanced features to take your project to the next level.
+
+export default ({ children }) => {children}
diff --git a/examples/README.md b/examples/README.md
index 32ca57d81f2..698dc576a83 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -18,7 +18,7 @@ You can use these as a starting point for general experimentation in a clean env
Each of the examples below demonstrates a particular feature of Keystone.
You can use these projects to learn about, and experiment with specific features.
-(coming soon)
+ * [`withAuth`](./with-auth): Adding password based authentication to your Keystone application.
## Solution Examples
diff --git a/examples/with-auth/CHANGELOG.md b/examples/with-auth/CHANGELOG.md
new file mode 100644
index 00000000000..b52cadc6fe3
--- /dev/null
+++ b/examples/with-auth/CHANGELOG.md
@@ -0,0 +1 @@
+# @keystone-next/example-with-auth
diff --git a/examples/with-auth/README.md b/examples/with-auth/README.md
new file mode 100644
index 00000000000..9d355b95950
--- /dev/null
+++ b/examples/with-auth/README.md
@@ -0,0 +1,93 @@
+## Feature Example - Authentication
+
+This project demonstrates how to add password based authentication to your Keystone system.
+It builds on the [todo](../) starter project.
+
+## Instructions
+
+To run this project, clone the Keystone repository locally then navigate to this directory and run:
+
+```shell
+yarn dev
+```
+
+This will start the Admin UI at [localhost:3000](http://localhost:3000).
+You can use the Admin UI to create items in your database.
+
+You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations.
+
+## Features
+
+This project shows you how to add authentication to your Keystone system. We're going to use the [`@keystone-next/auth`](https://next.keystonejs.com/apis/auth) package, along with Keystone's [session management API](https://next.keystonejs.com/apis/session), to add the following features to your system:
+
+ * Configures which fields to use for signin
+ * Sets up stateless session handling to keep track of the signed in user
+ * Adds a signin screen to the Admin UI
+ * Adds a signout button to the Admin UI
+ * Allows the signed in user to access their own details in the Admin UI
+ * Adds a helper page to the Admin UI to allow you to create your first user when starting from an empty database
+
+### Added fields
+
+We start by adding two new fields, `email` and `password`, to the `Person` list.
+These will be used as our _identity_ and _secret_ fields for login.
+
+```typescript
+ email: text({ isRequired: true, isUnique: true }),
+ password: password({ isRequired: true }),
+```
+
+### Auth config
+
+```typescript
+const { withAuth } = createAuth({
+ listKey: 'Person',
+ identityField: 'email',
+ secretField: 'password',
+ initFirstItem: { fields: ['name', 'email', 'password'] },
+});
+```
+
+### Session
+
+```typescript
+const session = statelessSessions({ secret: '-- EXAMPLE COOKIE SECRET; CHANGE ME --' });
+```
+
+### Wrapped config
+
+```typescript
+export default withAuth(
+ config({
+ db: {
+ provider: 'sqlite',
+ url: process.env.DATABASE_URL || 'file:./keystone-example.db',
+ },
+ lists,
+ session,
+ })
+);
+```
+
+## Screenshots
+
+### Admin UI
+
+Screenshots of the first item experience, mailing list experience(?), login screen, the "logged in as" bit, logout button.
+![initial user screen](../../screenshots/init-user-01.png)
+![mailing list subscription screen](../../screenshots/mailing-list-01.png)
+![sign in screen](../../screenshots/sign-in-screen-01.png)
+
+### GraphQL Playground
+
+Show off the new API that this has added and how to use it
+
+### Somehow show session cookie data.
+
+## Next steps
+
+This project is a bare bones system, and doesn't use any of Keystone's advanced features.
+We encourage you to experiment with the code here to see how Keystone works, become familiar with the Admin UI, and learn about the GraphQL Playground.
+
+Once you've got the hang of using this project, you can check out the [feature examples](../).
+These projects build on this starter project and show you how to use Keystones advanced features to take your project to the next level.
diff --git a/examples/with-auth/keystone.ts b/examples/with-auth/keystone.ts
new file mode 100644
index 00000000000..c9c939103ed
--- /dev/null
+++ b/examples/with-auth/keystone.ts
@@ -0,0 +1,45 @@
+import { config } from '@keystone-next/keystone/schema';
+import { statelessSessions } from '@keystone-next/keystone/session';
+import { createAuth } from '@keystone-next/auth';
+import { lists } from './schema';
+
+// createAuth configures signin functionality based on the config below. Note this only implements
+// authentication, i.e signing in as an item using identity and secret fields in a list. Session
+// management and access control are controlled independently in the main keystone config.
+const { withAuth } = createAuth({
+ // This is the list that contains items people can sign in as
+ listKey: 'Person',
+ // The identity field is typically a username or email address
+ identityField: 'email',
+ // The secret field must be a password type field
+ secretField: 'password',
+
+ // initFirstItem turns on the "First User" experience, which prompts you to create a new user
+ // when there are no items in the list yet
+ initFirstItem: {
+ // These fields are collected in the "Create First User" form
+ fields: ['name', 'email', 'password'],
+ },
+});
+
+// Stateless sessions will store the listKey and itemId of the signed-in user in a cookie.
+// This session object will be made availble on the context object used in hooks, access-control,
+// resolvers, etc.
+const session = statelessSessions({
+ // The session secret is used to encrypt cookie data (should be an environment variable)
+ secret: '-- EXAMPLE COOKIE SECRET; CHANGE ME --',
+});
+
+// We wrap our config using the withAuth function. This will inject all
+// the extra config required to add support for authentication in our system.
+export default withAuth(
+ config({
+ db: {
+ provider: 'sqlite',
+ url: process.env.DATABASE_URL || 'file:./keystone-example.db',
+ },
+ lists,
+ // We add our session configuration to the system here.
+ session,
+ })
+);
diff --git a/examples/with-auth/package.json b/examples/with-auth/package.json
new file mode 100644
index 00000000000..b4721775ac5
--- /dev/null
+++ b/examples/with-auth/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@keystone-next/example-with-auth",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "scripts": {
+ "dev": "keystone-next dev",
+ "start": "keystone-next start",
+ "build": "keystone-next build"
+ },
+ "dependencies": {
+ "@keystone-next/auth": "^23.0.0",
+ "@keystone-next/fields": "^8.0.0",
+ "@keystone-next/keystone": "^17.0.0"
+ },
+ "devDependencies": {
+ "typescript": "^4.2.4"
+ },
+ "engines": {
+ "node": ">=v12.13.1"
+ },
+ "repository": "https://github.com/keystonejs/keystone/tree/master/examples/with-auth"
+}
diff --git a/examples/with-auth/schema.graphql b/examples/with-auth/schema.graphql
new file mode 100644
index 00000000000..4de805e37b7
--- /dev/null
+++ b/examples/with-auth/schema.graphql
@@ -0,0 +1,470 @@
+enum TaskPriorityType {
+ low
+ medium
+ high
+}
+
+input PersonRelateToOneInput {
+ create: PersonCreateInput
+ connect: PersonWhereUniqueInput
+ disconnect: PersonWhereUniqueInput
+ disconnectAll: Boolean
+}
+
+"""
+ A keystone list
+"""
+type Task {
+ id: ID!
+ label: String
+ priority: TaskPriorityType
+ isComplete: Boolean
+ assignedTo: Person
+ finishBy: String
+}
+
+input TaskWhereInput {
+ AND: [TaskWhereInput]
+ OR: [TaskWhereInput]
+ id: ID
+ id_not: ID
+ id_lt: ID
+ id_lte: ID
+ id_gt: ID
+ id_gte: ID
+ id_in: [ID]
+ id_not_in: [ID]
+ label: String
+ label_not: String
+ label_contains: String
+ label_not_contains: String
+ label_in: [String]
+ label_not_in: [String]
+ priority: TaskPriorityType
+ priority_not: TaskPriorityType
+ priority_in: [TaskPriorityType]
+ priority_not_in: [TaskPriorityType]
+ isComplete: Boolean
+ isComplete_not: Boolean
+ assignedTo: PersonWhereInput
+ assignedTo_is_null: Boolean
+ finishBy: String
+ finishBy_not: String
+ finishBy_lt: String
+ finishBy_lte: String
+ finishBy_gt: String
+ finishBy_gte: String
+ finishBy_in: [String]
+ finishBy_not_in: [String]
+}
+
+input TaskWhereUniqueInput {
+ id: ID!
+}
+
+enum SortTasksBy {
+ id_ASC
+ id_DESC
+ label_ASC
+ label_DESC
+ priority_ASC
+ priority_DESC
+ isComplete_ASC
+ isComplete_DESC
+ finishBy_ASC
+ finishBy_DESC
+}
+
+input TaskUpdateInput {
+ label: String
+ priority: TaskPriorityType
+ isComplete: Boolean
+ assignedTo: PersonRelateToOneInput
+ finishBy: String
+}
+
+input TasksUpdateInput {
+ id: ID!
+ data: TaskUpdateInput
+}
+
+input TaskCreateInput {
+ label: String
+ priority: TaskPriorityType
+ isComplete: Boolean
+ assignedTo: PersonRelateToOneInput
+ finishBy: String
+}
+
+input TasksCreateInput {
+ data: TaskCreateInput
+}
+
+input TaskRelateToManyInput {
+ create: [TaskCreateInput]
+ connect: [TaskWhereUniqueInput]
+ disconnect: [TaskWhereUniqueInput]
+ disconnectAll: Boolean
+}
+
+"""
+ A keystone list
+"""
+type Person {
+ id: ID!
+ name: String
+ email: String
+ password_is_set: Boolean
+ tasks(
+ where: TaskWhereInput
+ search: String
+ sortBy: [SortTasksBy!]
+ orderBy: String
+ first: Int
+ skip: Int
+ ): [Task!]!
+ _tasksMeta(
+ where: TaskWhereInput
+ search: String
+ sortBy: [SortTasksBy!]
+ orderBy: String
+ first: Int
+ skip: Int
+ ): _QueryMeta
+}
+
+input PersonWhereInput {
+ AND: [PersonWhereInput]
+ OR: [PersonWhereInput]
+ id: ID
+ id_not: ID
+ id_lt: ID
+ id_lte: ID
+ id_gt: ID
+ id_gte: ID
+ id_in: [ID]
+ id_not_in: [ID]
+ name: String
+ name_not: String
+ name_contains: String
+ name_not_contains: String
+ name_in: [String]
+ name_not_in: [String]
+ email: String
+ email_not: String
+ email_contains: String
+ email_not_contains: String
+ email_in: [String]
+ email_not_in: [String]
+ password_is_set: Boolean
+
+ """
+ condition must be true for all nodes
+ """
+ tasks_every: TaskWhereInput
+
+ """
+ condition must be true for at least 1 node
+ """
+ tasks_some: TaskWhereInput
+
+ """
+ condition must be false for all nodes
+ """
+ tasks_none: TaskWhereInput
+}
+
+input PersonWhereUniqueInput {
+ id: ID!
+}
+
+enum SortPeopleBy {
+ id_ASC
+ id_DESC
+ name_ASC
+ name_DESC
+ email_ASC
+ email_DESC
+}
+
+input PersonUpdateInput {
+ name: String
+ email: String
+ password: String
+ tasks: TaskRelateToManyInput
+}
+
+input PeopleUpdateInput {
+ id: ID!
+ data: PersonUpdateInput
+}
+
+input PersonCreateInput {
+ name: String
+ email: String
+ password: String
+ tasks: TaskRelateToManyInput
+}
+
+input PeopleCreateInput {
+ data: PersonCreateInput
+}
+
+"""
+The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
+"""
+scalar JSON
+ @specifiedBy(
+ url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"
+ )
+
+type _QueryMeta {
+ count: Int
+}
+
+type Mutation {
+ """
+ Create a single Task item.
+ """
+ createTask(data: TaskCreateInput): Task
+
+ """
+ Create multiple Task items.
+ """
+ createTasks(data: [TasksCreateInput]): [Task]
+
+ """
+ Update a single Task item by ID.
+ """
+ updateTask(id: ID!, data: TaskUpdateInput): Task
+
+ """
+ Update multiple Task items by ID.
+ """
+ updateTasks(data: [TasksUpdateInput]): [Task]
+
+ """
+ Delete a single Task item by ID.
+ """
+ deleteTask(id: ID!): Task
+
+ """
+ Delete multiple Task items by ID.
+ """
+ deleteTasks(ids: [ID!]): [Task]
+
+ """
+ Create a single Person item.
+ """
+ createPerson(data: PersonCreateInput): Person
+
+ """
+ Create multiple Person items.
+ """
+ createPeople(data: [PeopleCreateInput]): [Person]
+
+ """
+ Update a single Person item by ID.
+ """
+ updatePerson(id: ID!, data: PersonUpdateInput): Person
+
+ """
+ Update multiple Person items by ID.
+ """
+ updatePeople(data: [PeopleUpdateInput]): [Person]
+
+ """
+ Delete a single Person item by ID.
+ """
+ deletePerson(id: ID!): Person
+
+ """
+ Delete multiple Person items by ID.
+ """
+ deletePeople(ids: [ID!]): [Person]
+ authenticatePersonWithPassword(
+ email: String!
+ password: String!
+ ): PersonAuthenticationWithPasswordResult!
+ createInitialPerson(
+ data: CreateInitialPersonInput!
+ ): PersonAuthenticationWithPasswordSuccess!
+ endSession: Boolean!
+}
+
+"""
+The `Upload` scalar type represents a file upload.
+"""
+scalar Upload
+
+union AuthenticatedItem = Person
+
+union PersonAuthenticationWithPasswordResult =
+ PersonAuthenticationWithPasswordSuccess
+ | PersonAuthenticationWithPasswordFailure
+
+type PersonAuthenticationWithPasswordSuccess {
+ sessionToken: String!
+ item: Person!
+}
+
+type PersonAuthenticationWithPasswordFailure {
+ code: PasswordAuthErrorCode!
+ message: String!
+}
+
+enum PasswordAuthErrorCode {
+ FAILURE
+ IDENTITY_NOT_FOUND
+ SECRET_NOT_SET
+ MULTIPLE_IDENTITY_MATCHES
+ SECRET_MISMATCH
+}
+
+input CreateInitialPersonInput {
+ name: String
+ email: String
+ password: String
+}
+
+type Query {
+ """
+ Search for all Task items which match the where clause.
+ """
+ allTasks(
+ where: TaskWhereInput
+ search: String
+ sortBy: [SortTasksBy!]
+ orderBy: String
+ first: Int
+ skip: Int
+ ): [Task]
+
+ """
+ Search for the Task item with the matching ID.
+ """
+ Task(where: TaskWhereUniqueInput!): Task
+
+ """
+ Perform a meta-query on all Task items which match the where clause.
+ """
+ _allTasksMeta(
+ where: TaskWhereInput
+ search: String
+ sortBy: [SortTasksBy!]
+ orderBy: String
+ first: Int
+ skip: Int
+ ): _QueryMeta
+
+ """
+ Search for all Person items which match the where clause.
+ """
+ allPeople(
+ where: PersonWhereInput
+ search: String
+ sortBy: [SortPeopleBy!]
+ orderBy: String
+ first: Int
+ skip: Int
+ ): [Person]
+
+ """
+ Search for the Person item with the matching ID.
+ """
+ Person(where: PersonWhereUniqueInput!): Person
+
+ """
+ Perform a meta-query on all Person items which match the where clause.
+ """
+ _allPeopleMeta(
+ where: PersonWhereInput
+ search: String
+ sortBy: [SortPeopleBy!]
+ orderBy: String
+ first: Int
+ skip: Int
+ ): _QueryMeta
+ authenticatedItem: AuthenticatedItem
+ keystone: KeystoneMeta!
+}
+
+type KeystoneMeta {
+ adminMeta: KeystoneAdminMeta!
+}
+
+type KeystoneAdminMeta {
+ enableSignout: Boolean!
+ enableSessionItem: Boolean!
+ lists: [KeystoneAdminUIListMeta!]!
+ list(key: String!): KeystoneAdminUIListMeta
+}
+
+type KeystoneAdminUIListMeta {
+ key: String!
+ itemQueryName: String!
+ listQueryName: String!
+ hideCreate: Boolean!
+ hideDelete: Boolean!
+ path: String!
+ label: String!
+ singular: String!
+ plural: String!
+ description: String
+ initialColumns: [String!]!
+ pageSize: Int!
+ labelField: String!
+ fields: [KeystoneAdminUIFieldMeta!]!
+ initialSort: KeystoneAdminUISort
+ isHidden: Boolean!
+}
+
+type KeystoneAdminUIFieldMeta {
+ path: String!
+ label: String!
+ isOrderable: Boolean!
+ fieldMeta: JSON
+ viewsIndex: Int!
+ customViewsIndex: Int
+ createView: KeystoneAdminUIFieldMetaCreateView!
+ listView: KeystoneAdminUIFieldMetaListView!
+ itemView(id: ID!): KeystoneAdminUIFieldMetaItemView
+}
+
+type KeystoneAdminUIFieldMetaCreateView {
+ fieldMode: KeystoneAdminUIFieldMetaCreateViewFieldMode!
+}
+
+enum KeystoneAdminUIFieldMetaCreateViewFieldMode {
+ edit
+ hidden
+}
+
+type KeystoneAdminUIFieldMetaListView {
+ fieldMode: KeystoneAdminUIFieldMetaListViewFieldMode!
+}
+
+enum KeystoneAdminUIFieldMetaListViewFieldMode {
+ read
+ hidden
+}
+
+type KeystoneAdminUIFieldMetaItemView {
+ fieldMode: KeystoneAdminUIFieldMetaItemViewFieldMode!
+}
+
+enum KeystoneAdminUIFieldMetaItemViewFieldMode {
+ edit
+ read
+ hidden
+}
+
+type KeystoneAdminUISort {
+ field: String!
+ direction: KeystoneAdminUISortDirection!
+}
+
+enum KeystoneAdminUISortDirection {
+ ASC
+ DESC
+}
diff --git a/examples/with-auth/schema.prisma b/examples/with-auth/schema.prisma
new file mode 100644
index 00000000000..40835e596da
--- /dev/null
+++ b/examples/with-auth/schema.prisma
@@ -0,0 +1,29 @@
+datasource sqlite {
+ url = env("DATABASE_URL")
+ provider = "sqlite"
+}
+
+generator client {
+ provider = "prisma-client-js"
+ output = "node_modules/.prisma/client"
+}
+
+model Task {
+ id Int @id @default(autoincrement())
+ label String?
+ priority String?
+ isComplete Boolean?
+ finishBy DateTime?
+ assignedTo Person? @relation("TaskassignedTo", fields: [assignedToId], references: [id])
+ assignedToId Int? @map("assignedTo")
+
+ @@index([assignedToId])
+}
+
+model Person {
+ id Int @id @default(autoincrement())
+ name String?
+ email String? @unique
+ password String?
+ tasks Task[] @relation("TaskassignedTo")
+}
\ No newline at end of file
diff --git a/examples/with-auth/schema.ts b/examples/with-auth/schema.ts
new file mode 100644
index 00000000000..4be26920552
--- /dev/null
+++ b/examples/with-auth/schema.ts
@@ -0,0 +1,36 @@
+import { createSchema, list } from '@keystone-next/keystone/schema';
+import { checkbox, password, relationship, text, timestamp } from '@keystone-next/fields';
+import { select } from '@keystone-next/fields';
+
+export const lists = createSchema({
+ Task: list({
+ fields: {
+ label: text({ isRequired: true }),
+ priority: select({
+ dataType: 'enum',
+ options: [
+ { label: 'Low', value: 'low' },
+ { label: 'Medium', value: 'medium' },
+ { label: 'High', value: 'high' },
+ ],
+ }),
+ isComplete: checkbox(),
+ assignedTo: relationship({ ref: 'Person.tasks', many: false }),
+ finishBy: timestamp(),
+ },
+ }),
+ Person: list({
+ fields: {
+ name: text({ isRequired: true }),
+ // Added an email and password pair to be used with authentication
+ // The email address is going to be used as the identity field, so it's
+ // important that we set both isRequired and isUnique
+ email: text({ isRequired: true, isUnique: true }),
+ // The password field stores a hash of the supplied password, and
+ // we want to ensure that all people have a password set, so we use
+ // the isRequired flag.
+ password: password({ isRequired: true }),
+ tasks: relationship({ ref: 'Task.assignedTo', many: true }),
+ },
+ }),
+});
diff --git a/screenshots/init-user-01.png b/screenshots/init-user-01.png
new file mode 100644
index 00000000000..7c3af0383fc
Binary files /dev/null and b/screenshots/init-user-01.png differ
diff --git a/screenshots/mailing-list-01.png b/screenshots/mailing-list-01.png
new file mode 100644
index 00000000000..983882b67ee
Binary files /dev/null and b/screenshots/mailing-list-01.png differ
diff --git a/screenshots/sign-in-screen-01.png b/screenshots/sign-in-screen-01.png
new file mode 100644
index 00000000000..f0ab01adbe7
Binary files /dev/null and b/screenshots/sign-in-screen-01.png differ