diff --git a/lib/client/storyFormSchema.ts b/lib/client/storyFormSchema.ts
new file mode 100644
index 0000000..aeface6
--- /dev/null
+++ b/lib/client/storyFormSchema.ts
@@ -0,0 +1,107 @@
+import * as yup from "yup";
+import { FormChapter, FormStory } from "./types/form/story";
+
+const emptySummary = "Summary cannot be blank";
+const emptyChapterTitle = "Chapter title cannot be blank.";
+const blankTitle = "Title cannot be blank.";
+
+export const cs = yup.object<FormChapter>().shape({
+	chapterTitle: yup
+		.string()
+		.ensure()
+		.min(1, emptyChapterTitle)
+		.trim(emptyChapterTitle)
+		.required(emptyChapterTitle),
+	summary: yup.string().ensure().min(10, emptySummary).required(emptySummary),
+	notes: yup.string().ensure(),
+	bands: yup
+		.array()
+		.ensure()
+		.of(yup.number())
+		.min(1, "One or more bands must be selected."),
+	characters: yup
+		.array()
+		.ensure()
+		.min(1, "One or more characters must be selected"),
+	relationships: yup
+		.array()
+		.ensure()
+		.of(
+			yup
+				.array()
+				.ensure()
+				.of(yup.string())
+				.min(2, "Pairings must have at least two characters!")
+				.max(3, "Pairings can have no more than three characters!"),
+		),
+	nsfw: yup.boolean().oneOf([true, false]),
+	loggedInOnly: yup.boolean().when("nsfw", ([nsfw], schema) => {
+		return nsfw
+			? schema.oneOf(
+					[true],
+					"If your story contains adult content, you MUST restrict the ability to read it to logged-in users only. Failure to comply may result in a takedown.",
+			  )
+			: schema.oneOf([true, false]);
+	}),
+	hidden: yup.boolean().oneOf([true, false]),
+	pot: yup
+		.string()
+		.oneOf(["pasteOrType", "upload"])
+		.required("Story content is required!"),
+	storytext: yup.string().when("pot", ([pot], schema) => {
+		return pot === "pasteOrType"
+			? schema
+					.test(
+						"numWords",
+						"Story must be at least 50 words",
+						(value: any, context) => {
+							return value?.split(/\W+/).length > 50 || false;
+						},
+					)
+					.required("Story text can't be blank!")
+			: schema.min(0);
+	}),
+	// hello Celeste
+	file: yup.mixed().when(
+		"pot",
+		([pot], schema) => {
+			return pot === "upload"
+				? yup.string().ensure().trim().required("file name required")
+				: /* .mixed()
+						.test("exists", "You need to upload a file!", (value) => {
+							// console.debug("file: ", value)
+							return !!value;
+						})
+						.test(
+							"fileType",
+							"Supported files are *.docx and *.doc",
+							(value) => {
+								let ext;
+								if (typeof value == "string") {
+									ext = value?.split(".").reverse()[0];
+								} else {
+									ext = (value as File)?.name?.split(".").reverse()[0];
+								}
+								// console.log(ext)
+								let reg = /(docx|doc)$/i;
+								return reg.test(ext);
+							},
+						) */
+				  yup.mixed();
+		} /* {
+		is: "upload",
+		then: ,
+		otherwise: yup.mixed()
+	} */,
+	),
+});
+
+export const storySchema = yup.object().shape({
+	title: yup.string().ensure().min(5, blankTitle).required(blankTitle),
+	chapters: yup
+		.array()
+		.min(1, "There must be at least one chapter.")
+		.of(cs)
+		.ensure(),
+	completed: yup.boolean().oneOf([true, false]),
+});